mirror of
https://github.com/pmmp/PocketMine-MP.git
synced 2025-04-20 16:00:20 +00:00
Merge branch 'item-stack-request' into minor-next
This commit is contained in:
commit
8408da8534
@ -37,7 +37,7 @@
|
||||
"pocketmine/bedrock-block-upgrade-schema": "~1.1.1+bedrock-1.19.70",
|
||||
"pocketmine/bedrock-data": "~2.1.1+bedrock-1.19.70",
|
||||
"pocketmine/bedrock-item-upgrade-schema": "~1.1.0+bedrock-1.19.70",
|
||||
"pocketmine/bedrock-protocol": "~20.0.0+bedrock-1.19.70",
|
||||
"pocketmine/bedrock-protocol": "~20.1.0+bedrock-1.19.70",
|
||||
"pocketmine/binaryutils": "^0.2.1",
|
||||
"pocketmine/callback-validator": "^1.0.2",
|
||||
"pocketmine/classloader": "^0.2.0",
|
||||
|
16
composer.lock
generated
16
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": "1d0c1d2fe668d85ae87110a1e3cfac05",
|
||||
"content-hash": "01afa65b40f95ad9378c8cd999e6098d",
|
||||
"packages": [
|
||||
{
|
||||
"name": "adhocore/json-comment",
|
||||
@ -328,16 +328,16 @@
|
||||
},
|
||||
{
|
||||
"name": "pocketmine/bedrock-protocol",
|
||||
"version": "20.0.0+bedrock-1.19.70",
|
||||
"version": "20.1.0+bedrock-1.19.70",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/pmmp/BedrockProtocol.git",
|
||||
"reference": "4892a5020187da805d7b46ab522d8185b0283726"
|
||||
"reference": "91d67c8b1bced3c82d0841b1041c0c1f4e93eb68"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/pmmp/BedrockProtocol/zipball/4892a5020187da805d7b46ab522d8185b0283726",
|
||||
"reference": "4892a5020187da805d7b46ab522d8185b0283726",
|
||||
"url": "https://api.github.com/repos/pmmp/BedrockProtocol/zipball/91d67c8b1bced3c82d0841b1041c0c1f4e93eb68",
|
||||
"reference": "91d67c8b1bced3c82d0841b1041c0c1f4e93eb68",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@ -351,7 +351,7 @@
|
||||
"ramsey/uuid": "^4.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpstan/phpstan": "1.10.1",
|
||||
"phpstan/phpstan": "1.10.7",
|
||||
"phpstan/phpstan-phpunit": "^1.0.0",
|
||||
"phpstan/phpstan-strict-rules": "^1.0.0",
|
||||
"phpunit/phpunit": "^9.5"
|
||||
@ -369,9 +369,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/20.0.0+bedrock-1.19.70"
|
||||
"source": "https://github.com/pmmp/BedrockProtocol/tree/20.1.0+bedrock-1.19.70"
|
||||
},
|
||||
"time": "2023-03-14T17:06:38+00:00"
|
||||
"time": "2023-03-20T01:17:00+00:00"
|
||||
},
|
||||
{
|
||||
"name": "pocketmine/binaryutils",
|
||||
|
@ -45,6 +45,12 @@ class CraftingManager{
|
||||
*/
|
||||
protected $shapelessRecipes = [];
|
||||
|
||||
/**
|
||||
* @var CraftingRecipe[]
|
||||
* @phpstan-var array<int, CraftingRecipe>
|
||||
*/
|
||||
private array $craftingRecipeIndex = [];
|
||||
|
||||
/**
|
||||
* @var FurnaceRecipeManager[]
|
||||
* @phpstan-var array<int, FurnaceRecipeManager>
|
||||
@ -153,6 +159,18 @@ class CraftingManager{
|
||||
return $this->shapedRecipes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return CraftingRecipe[]
|
||||
* @phpstan-return array<int, CraftingRecipe>
|
||||
*/
|
||||
public function getCraftingRecipeIndex() : array{
|
||||
return $this->craftingRecipeIndex;
|
||||
}
|
||||
|
||||
public function getCraftingRecipeFromIndex(int $index) : ?CraftingRecipe{
|
||||
return $this->craftingRecipeIndex[$index] ?? null;
|
||||
}
|
||||
|
||||
public function getFurnaceRecipeManager(FurnaceType $furnaceType) : FurnaceRecipeManager{
|
||||
return $this->furnaceRecipeManagers[$furnaceType->id()];
|
||||
}
|
||||
@ -175,6 +193,7 @@ class CraftingManager{
|
||||
|
||||
public function registerShapedRecipe(ShapedRecipe $recipe) : void{
|
||||
$this->shapedRecipes[self::hashOutputs($recipe->getResults())][] = $recipe;
|
||||
$this->craftingRecipeIndex[] = $recipe;
|
||||
|
||||
foreach($this->recipeRegisteredCallbacks as $callback){
|
||||
$callback();
|
||||
@ -183,6 +202,7 @@ class CraftingManager{
|
||||
|
||||
public function registerShapelessRecipe(ShapelessRecipe $recipe) : void{
|
||||
$this->shapelessRecipes[self::hashOutputs($recipe->getResults())][] = $recipe;
|
||||
$this->craftingRecipeIndex[] = $recipe;
|
||||
|
||||
foreach($this->recipeRegisteredCallbacks as $callback){
|
||||
$callback();
|
||||
|
@ -348,7 +348,7 @@ abstract class BaseInventory implements Inventory{
|
||||
if($invManager === null){
|
||||
continue;
|
||||
}
|
||||
$invManager->syncSlot($this, $index);
|
||||
$invManager->onSlotChange($this, $index);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -60,9 +60,11 @@ class CraftingTransaction extends InventoryTransaction{
|
||||
|
||||
private CraftingManager $craftingManager;
|
||||
|
||||
public function __construct(Player $source, CraftingManager $craftingManager, array $actions = []){
|
||||
public function __construct(Player $source, CraftingManager $craftingManager, array $actions = [], ?CraftingRecipe $recipe = null, ?int $repetitions = null){
|
||||
parent::__construct($source, $actions);
|
||||
$this->craftingManager = $craftingManager;
|
||||
$this->recipe = $recipe;
|
||||
$this->repetitions = $repetitions;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -123,6 +125,18 @@ class CraftingTransaction extends InventoryTransaction{
|
||||
return $iterations;
|
||||
}
|
||||
|
||||
private function validateRecipe(CraftingRecipe $recipe, ?int $expectedRepetitions) : int{
|
||||
//compute number of times recipe was crafted
|
||||
$repetitions = $this->matchRecipeItems($this->outputs, $recipe->getResultsFor($this->source->getCraftingGrid()), false);
|
||||
if($expectedRepetitions !== null && $repetitions !== $expectedRepetitions){
|
||||
throw new TransactionValidationException("Expected $expectedRepetitions repetitions, got $repetitions");
|
||||
}
|
||||
//assert that $repetitions x recipe ingredients should be consumed
|
||||
$this->matchRecipeItems($this->inputs, $recipe->getIngredientList(), true, $repetitions);
|
||||
|
||||
return $repetitions;
|
||||
}
|
||||
|
||||
public function validate() : void{
|
||||
$this->squashDuplicateSlotChanges();
|
||||
if(count($this->actions) < 1){
|
||||
@ -131,25 +145,24 @@ class CraftingTransaction extends InventoryTransaction{
|
||||
|
||||
$this->matchItems($this->outputs, $this->inputs);
|
||||
|
||||
$failed = 0;
|
||||
foreach($this->craftingManager->matchRecipeByOutputs($this->outputs) as $recipe){
|
||||
try{
|
||||
//compute number of times recipe was crafted
|
||||
$this->repetitions = $this->matchRecipeItems($this->outputs, $recipe->getResultsFor($this->source->getCraftingGrid()), false);
|
||||
//assert that $repetitions x recipe ingredients should be consumed
|
||||
$this->matchRecipeItems($this->inputs, $recipe->getIngredientList(), true, $this->repetitions);
|
||||
|
||||
//Success!
|
||||
$this->recipe = $recipe;
|
||||
break;
|
||||
}catch(TransactionValidationException $e){
|
||||
//failed
|
||||
++$failed;
|
||||
}
|
||||
}
|
||||
|
||||
if($this->recipe === null){
|
||||
throw new TransactionValidationException("Unable to match a recipe to transaction (tried to match against $failed recipes)");
|
||||
$failed = 0;
|
||||
foreach($this->craftingManager->matchRecipeByOutputs($this->outputs) as $recipe){
|
||||
try{
|
||||
$this->repetitions = $this->validateRecipe($recipe, $this->repetitions);
|
||||
$this->recipe = $recipe;
|
||||
break;
|
||||
}catch(TransactionValidationException $e){
|
||||
//failed
|
||||
++$failed;
|
||||
}
|
||||
}
|
||||
|
||||
if($this->recipe === null){
|
||||
throw new TransactionValidationException("Unable to match a recipe to transaction (tried to match against $failed recipes)");
|
||||
}
|
||||
}else{
|
||||
$this->repetitions = $this->validateRecipe($this->recipe, $this->repetitions);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -50,6 +50,10 @@ final class TransactionBuilderInventory extends BaseInventory{
|
||||
$this->changedSlots = new \SplFixedArray($this->actualInventory->getSize());
|
||||
}
|
||||
|
||||
public function getActualInventory() : Inventory{
|
||||
return $this->actualInventory;
|
||||
}
|
||||
|
||||
protected function internalSetContents(array $items) : void{
|
||||
for($i = 0, $size = $this->getSize(); $i < $size; ++$i){
|
||||
if(!isset($items[$i])){
|
||||
|
@ -25,7 +25,7 @@ namespace pocketmine\network\mcpe;
|
||||
|
||||
use pocketmine\inventory\Inventory;
|
||||
|
||||
final class ComplexWindowMapEntry{
|
||||
final class ComplexInventoryMapEntry{
|
||||
|
||||
/**
|
||||
* @var int[]
|
@ -38,7 +38,6 @@ use pocketmine\inventory\Inventory;
|
||||
use pocketmine\inventory\transaction\action\SlotChangeAction;
|
||||
use pocketmine\inventory\transaction\InventoryTransaction;
|
||||
use pocketmine\item\Item;
|
||||
use pocketmine\network\mcpe\convert\TypeConversionException;
|
||||
use pocketmine\network\mcpe\convert\TypeConverter;
|
||||
use pocketmine\network\mcpe\protocol\ClientboundPacket;
|
||||
use pocketmine\network\mcpe\protocol\ContainerClosePacket;
|
||||
@ -51,6 +50,7 @@ use pocketmine\network\mcpe\protocol\MobEquipmentPacket;
|
||||
use pocketmine\network\mcpe\protocol\types\BlockPosition;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\ContainerIds;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\CreativeContentEntry;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\ItemStack;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\ItemStackWrapper;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\NetworkInventoryAction;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\UIInventorySlotOffset;
|
||||
@ -59,9 +59,11 @@ use pocketmine\network\PacketHandlingException;
|
||||
use pocketmine\player\Player;
|
||||
use pocketmine\utils\AssumptionFailedError;
|
||||
use pocketmine\utils\ObjectSet;
|
||||
use function array_map;
|
||||
use function array_keys;
|
||||
use function array_search;
|
||||
use function count;
|
||||
use function get_class;
|
||||
use function implode;
|
||||
use function is_int;
|
||||
use function max;
|
||||
use function spl_object_id;
|
||||
@ -70,26 +72,25 @@ use function spl_object_id;
|
||||
* @phpstan-type ContainerOpenClosure \Closure(int $id, Inventory $inventory) : (list<ClientboundPacket>|null)
|
||||
*/
|
||||
class InventoryManager{
|
||||
/** @var Inventory[] */
|
||||
private array $windowMap = [];
|
||||
/**
|
||||
* @var ComplexWindowMapEntry[]
|
||||
* @phpstan-var array<int, ComplexWindowMapEntry>
|
||||
* @var InventoryManagerEntry[] spl_object_id(Inventory) => InventoryManagerEntry
|
||||
* @phpstan-var array<int, InventoryManagerEntry>
|
||||
*/
|
||||
private array $complexWindows = [];
|
||||
private array $inventories = [];
|
||||
|
||||
/**
|
||||
* @var ComplexWindowMapEntry[]
|
||||
* @phpstan-var array<int, ComplexWindowMapEntry>
|
||||
* @var Inventory[] network window ID => Inventory
|
||||
* @phpstan-var array<int, Inventory>
|
||||
*/
|
||||
private array $complexSlotToWindowMap = [];
|
||||
private array $networkIdToInventoryMap = [];
|
||||
/**
|
||||
* @var ComplexInventoryMapEntry[] net slot ID => ComplexWindowMapEntry
|
||||
* @phpstan-var array<int, ComplexInventoryMapEntry>
|
||||
*/
|
||||
private array $complexSlotToInventoryMap = [];
|
||||
|
||||
private int $lastInventoryNetworkId = ContainerIds::FIRST;
|
||||
|
||||
/**
|
||||
* @var Item[][]
|
||||
* @phpstan-var array<int, array<int, Item>>
|
||||
*/
|
||||
private array $initiatedSlotChanges = [];
|
||||
private int $clientSelectedHotbarSlot = -1;
|
||||
|
||||
/** @phpstan-var ObjectSet<ContainerOpenClosure> */
|
||||
@ -99,6 +100,11 @@ class InventoryManager{
|
||||
/** @phpstan-var \Closure() : void */
|
||||
private ?\Closure $pendingOpenWindowCallback = null;
|
||||
|
||||
private int $nextItemStackId = 1;
|
||||
private ?int $currentItemStackRequestId = null;
|
||||
|
||||
private bool $fullSyncRequested = false;
|
||||
|
||||
public function __construct(
|
||||
private Player $player,
|
||||
private NetworkSession $session
|
||||
@ -117,14 +123,27 @@ class InventoryManager{
|
||||
});
|
||||
}
|
||||
|
||||
private function associateIdWithInventory(int $id, Inventory $inventory) : void{
|
||||
$this->networkIdToInventoryMap[$id] = $inventory;
|
||||
}
|
||||
|
||||
private function getNewWindowId() : int{
|
||||
$this->lastInventoryNetworkId = max(ContainerIds::FIRST, ($this->lastInventoryNetworkId + 1) % ContainerIds::LAST);
|
||||
return $this->lastInventoryNetworkId;
|
||||
}
|
||||
|
||||
private function add(int $id, Inventory $inventory) : void{
|
||||
$this->windowMap[$id] = $inventory;
|
||||
if(isset($this->inventories[spl_object_id($inventory)])){
|
||||
throw new \InvalidArgumentException("Inventory " . get_class($inventory) . " is already tracked");
|
||||
}
|
||||
$this->inventories[spl_object_id($inventory)] = new InventoryManagerEntry($inventory);
|
||||
$this->associateIdWithInventory($id, $inventory);
|
||||
}
|
||||
|
||||
private function addDynamic(Inventory $inventory) : int{
|
||||
$this->lastInventoryNetworkId = max(ContainerIds::FIRST, ($this->lastInventoryNetworkId + 1) % ContainerIds::LAST);
|
||||
$this->add($this->lastInventoryNetworkId, $inventory);
|
||||
return $this->lastInventoryNetworkId;
|
||||
$id = $this->getNewWindowId();
|
||||
$this->add($id, $inventory);
|
||||
return $id;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -132,26 +151,45 @@ class InventoryManager{
|
||||
* @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;
|
||||
if(isset($this->inventories[spl_object_id($inventory)])){
|
||||
throw new \InvalidArgumentException("Inventory " . get_class($inventory) . " is already tracked");
|
||||
}
|
||||
$complexSlotMap = new ComplexInventoryMapEntry($inventory, is_int($slotMap) ? [$slotMap => 0] : $slotMap);
|
||||
$this->inventories[spl_object_id($inventory)] = new InventoryManagerEntry(
|
||||
$inventory,
|
||||
$complexSlotMap
|
||||
);
|
||||
foreach($complexSlotMap->getSlotMap() as $netSlot => $coreSlot){
|
||||
$this->complexSlotToInventoryMap[$netSlot] = $complexSlotMap;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int[]|int $slotMap
|
||||
* @phpstan-param array<int, int>|int $slotMap
|
||||
*/
|
||||
private function addComplexDynamic(array|int $slotMap, Inventory $inventory) : int{
|
||||
$this->addComplex($slotMap, $inventory);
|
||||
$id = $this->getNewWindowId();
|
||||
$this->associateIdWithInventory($id, $inventory);
|
||||
return $id;
|
||||
}
|
||||
|
||||
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]);
|
||||
$inventory = $this->networkIdToInventoryMap[$id];
|
||||
unset($this->networkIdToInventoryMap[$id]);
|
||||
if($this->getWindowId($inventory) === null){
|
||||
unset($this->inventories[spl_object_id($inventory)]);
|
||||
foreach($this->complexSlotToInventoryMap as $netSlot => $entry){
|
||||
if($entry->getInventory() === $inventory){
|
||||
unset($this->complexSlotToInventoryMap[$netSlot]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function getWindowId(Inventory $inventory) : ?int{
|
||||
return ($id = array_search($inventory, $this->windowMap, true)) !== false ? $id : null;
|
||||
return ($id = array_search($inventory, $this->networkIdToInventoryMap, true)) !== false ? $id : null;
|
||||
}
|
||||
|
||||
public function getCurrentWindowId() : int{
|
||||
@ -159,28 +197,33 @@ 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){
|
||||
$entry = $this->complexSlotToWindowMap[$netSlotId] ?? null;
|
||||
$entry = $this->complexSlotToInventoryMap[$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];
|
||||
if(isset($this->networkIdToInventoryMap[$windowId])){
|
||||
return [$this->networkIdToInventoryMap[$windowId], $netSlotId];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public function onTransactionStart(InventoryTransaction $tx) : void{
|
||||
private function addPredictedSlotChange(Inventory $inventory, int $slot, ItemStack $item) : void{
|
||||
$this->inventories[spl_object_id($inventory)]->predictions[$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){
|
||||
//TODO: ItemStackRequestExecutor can probably build these predictions with much lower overhead
|
||||
$itemStack = TypeConverter::getInstance()->coreItemStackToNet($action->getTargetItem());
|
||||
$this->addPredictedSlotChange($action->getInventory(), $action->getSlot(), $itemStack);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -189,22 +232,34 @@ 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]) ||
|
||||
($action->windowId === ContainerIds::UI && isset($this->complexSlotToWindowMap[$action->inventorySlot]))
|
||||
)){
|
||||
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;
|
||||
}
|
||||
|
||||
//legacy transactions should not modify or predict anything other than these inventories, since these are
|
||||
//the only ones accessible when not in-game (ItemStackRequest is used for everything else)
|
||||
if(match($action->windowId){
|
||||
ContainerIds::INVENTORY, ContainerIds::OFFHAND, ContainerIds::ARMOR => false,
|
||||
default => true
|
||||
}){
|
||||
throw new PacketHandlingException("Legacy transactions cannot predict changes to inventory with ID " . $action->windowId);
|
||||
}
|
||||
$info = $this->locateWindowAndSlot($action->windowId, $action->inventorySlot);
|
||||
if($info === null){
|
||||
continue;
|
||||
}
|
||||
|
||||
[$inventory, $slot] = $info;
|
||||
$this->addPredictedSlotChange($inventory, $slot, $action->newItem->getItemStack());
|
||||
}
|
||||
}
|
||||
|
||||
public function setCurrentItemStackRequestId(?int $id) : void{
|
||||
$this->currentItemStackRequestId = $id;
|
||||
}
|
||||
|
||||
/**
|
||||
* When the server initiates a window close, it does so by sending a ContainerClose to the client, which causes the
|
||||
* client to behave as if it initiated the close itself. It responds by sending a ContainerClose back to the server,
|
||||
@ -248,9 +303,10 @@ class InventoryManager{
|
||||
$this->onCurrentWindowRemove();
|
||||
|
||||
$this->openWindowDeferred(function() use ($inventory) : void{
|
||||
$windowId = $this->addDynamic($inventory);
|
||||
if(($slotMap = $this->createComplexSlotMapping($inventory)) !== null){
|
||||
$this->addComplex($slotMap, $inventory);
|
||||
$windowId = $this->addComplexDynamic($slotMap, $inventory);
|
||||
}else{
|
||||
$windowId = $this->addDynamic($inventory);
|
||||
}
|
||||
|
||||
foreach($this->containerOpenCallbacks as $callback){
|
||||
@ -304,7 +360,8 @@ class InventoryManager{
|
||||
$this->onCurrentWindowRemove();
|
||||
|
||||
$this->openWindowDeferred(function() : void{
|
||||
$windowId = $this->addDynamic($this->player->getInventory());
|
||||
$windowId = $this->getNewWindowId();
|
||||
$this->associateIdWithInventory($windowId, $this->player->getInventory());
|
||||
|
||||
$this->session->sendDataPacket(ContainerOpenPacket::entityInv(
|
||||
$windowId,
|
||||
@ -315,7 +372,7 @@ class InventoryManager{
|
||||
}
|
||||
|
||||
public function onCurrentWindowRemove() : void{
|
||||
if(isset($this->windowMap[$this->lastInventoryNetworkId])){
|
||||
if(isset($this->networkIdToInventoryMap[$this->lastInventoryNetworkId])){
|
||||
$this->remove($this->lastInventoryNetworkId);
|
||||
$this->session->sendDataPacket(ContainerClosePacket::create($this->lastInventoryNetworkId, true));
|
||||
if($this->pendingCloseWindowId !== null){
|
||||
@ -327,7 +384,7 @@ class InventoryManager{
|
||||
|
||||
public function onClientRemoveWindow(int $id) : void{
|
||||
if($id === $this->lastInventoryNetworkId){
|
||||
if(isset($this->windowMap[$id]) && $id !== $this->pendingCloseWindowId){
|
||||
if(isset($this->networkIdToInventoryMap[$id]) && $id !== $this->pendingCloseWindowId){
|
||||
$this->remove($id);
|
||||
$this->player->removeCurrentWindow();
|
||||
}
|
||||
@ -349,96 +406,147 @@ class InventoryManager{
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
public function onSlotChange(Inventory $inventory, int $slot) : void{
|
||||
$currentItem = TypeConverter::getInstance()->coreItemStackToNet($inventory->getItem($slot));
|
||||
$inventoryEntry = $this->inventories[spl_object_id($inventory)];
|
||||
$clientSideItem = $inventoryEntry->predictions[$slot] ?? null;
|
||||
if($clientSideItem === null || !$clientSideItem->equals($currentItem)){
|
||||
//no prediction or incorrect - do not associate this with the currently active itemstack request
|
||||
$this->trackItemStack($inventory, $slot, $currentItem, null);
|
||||
$inventoryEntry->pendingSyncs[$slot] = $slot;
|
||||
}else{
|
||||
$windowId = $this->getWindowId($inventory);
|
||||
//correctly predicted - associate the change with the currently active itemstack request
|
||||
$this->trackItemStack($inventory, $slot, $currentItem, $this->currentItemStackRequestId);
|
||||
}
|
||||
|
||||
unset($inventoryEntry->predictions[$slot]);
|
||||
}
|
||||
|
||||
public function syncSlot(Inventory $inventory, int $slot) : void{
|
||||
$entry = $this->inventories[spl_object_id($inventory)] ?? null;
|
||||
if($entry === null){
|
||||
throw new \LogicException("Cannot sync an untracked inventory");
|
||||
}
|
||||
$itemStackInfo = $entry->itemStackInfos[$slot];
|
||||
if($itemStackInfo === null){
|
||||
throw new \LogicException("Cannot sync an untracked inventory slot");
|
||||
}
|
||||
if($entry->complexSlotMap !== null){
|
||||
$windowId = ContainerIds::UI;
|
||||
$netSlot = $entry->complexSlotMap->mapCoreToNet($slot) ?? throw new AssumptionFailedError("We already have an ItemStackInfo, so this should not be null");
|
||||
}else{
|
||||
$windowId = $this->getWindowId($inventory) ?? throw new AssumptionFailedError("We already have an ItemStackInfo, so this should not be null");
|
||||
$netSlot = $slot;
|
||||
}
|
||||
if($windowId !== null && $netSlot !== null){
|
||||
$currentItem = $inventory->getItem($slot);
|
||||
$clientSideItem = $this->initiatedSlotChanges[$windowId][$netSlot] ?? null;
|
||||
if($clientSideItem === null || !$clientSideItem->equalsExact($currentItem)){
|
||||
$itemStackWrapper = ItemStackWrapper::legacy(TypeConverter::getInstance()->coreItemStackToNet($currentItem));
|
||||
if($windowId === ContainerIds::OFFHAND){
|
||||
//TODO: HACK!
|
||||
//The client may sometimes ignore the InventorySlotPacket for the offhand slot.
|
||||
//This can cause a lot of problems (totems, arrows, and more...).
|
||||
//The workaround is to send an InventoryContentPacket instead
|
||||
//BDS (Bedrock Dedicated Server) also seems to work this way.
|
||||
$this->session->sendDataPacket(InventoryContentPacket::create($windowId, [$itemStackWrapper]));
|
||||
}else{
|
||||
$this->session->sendDataPacket(InventorySlotPacket::create($windowId, $netSlot, $itemStackWrapper));
|
||||
}
|
||||
|
||||
$itemStackWrapper = new ItemStackWrapper($itemStackInfo->getStackId(), $itemStackInfo->getItemStack());
|
||||
if($windowId === ContainerIds::OFFHAND){
|
||||
//TODO: HACK!
|
||||
//The client may sometimes ignore the InventorySlotPacket for the offhand slot.
|
||||
//This can cause a lot of problems (totems, arrows, and more...).
|
||||
//The workaround is to send an InventoryContentPacket instead
|
||||
//BDS (Bedrock Dedicated Server) also seems to work this way.
|
||||
$this->session->sendDataPacket(InventoryContentPacket::create($windowId, [$itemStackWrapper]));
|
||||
}else{
|
||||
if($windowId === ContainerIds::ARMOR){
|
||||
//TODO: HACK!
|
||||
//When right-clicking to equip armour, the client predicts the content of the armour slot, but
|
||||
//doesn't report it in the transaction packet. The server then sends an InventorySlotPacket to
|
||||
//the client, assuming the slot changed for some other reason, since there is no prediction for
|
||||
//the slot.
|
||||
//However, later requests involving that itemstack will refer to the request ID in which the
|
||||
//armour was equipped, instead of the stack ID provided by the server in the outgoing
|
||||
//InventorySlotPacket. (Perhaps because the item is already the same as the client actually
|
||||
//predicted, but didn't tell us?)
|
||||
//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())));
|
||||
}
|
||||
unset($this->initiatedSlotChanges[$windowId][$netSlot]);
|
||||
$this->session->sendDataPacket(InventorySlotPacket::create($windowId, $netSlot, $itemStackWrapper));
|
||||
}
|
||||
unset($entry->predictions[$slot], $entry->pendingSyncs[$slot]);
|
||||
}
|
||||
|
||||
public function syncContents(Inventory $inventory) : void{
|
||||
$slotMap = $this->complexWindows[spl_object_id($inventory)] ?? null;
|
||||
if($slotMap !== null){
|
||||
$entry = $this->inventories[spl_object_id($inventory)];
|
||||
if($entry->complexSlotMap !== null){
|
||||
$windowId = ContainerIds::UI;
|
||||
}else{
|
||||
$windowId = $this->getWindowId($inventory);
|
||||
}
|
||||
$typeConverter = TypeConverter::getInstance();
|
||||
if($windowId !== null){
|
||||
if($slotMap !== null){
|
||||
foreach($inventory->getContents(true) as $slotId => $item){
|
||||
$packetSlot = $slotMap->mapCoreToNet($slotId) ?? null;
|
||||
$entry->predictions = [];
|
||||
$entry->pendingSyncs = [];
|
||||
$contents = [];
|
||||
foreach($inventory->getContents(true) as $slot => $item){
|
||||
$itemStack = TypeConverter::getInstance()->coreItemStackToNet($item);
|
||||
$info = $this->trackItemStack($inventory, $slot, $itemStack, null);
|
||||
$contents[] = new ItemStackWrapper($info->getStackId(), $info->getItemStack());
|
||||
}
|
||||
if($entry->complexSlotMap !== null){
|
||||
foreach($contents as $slotId => $info){
|
||||
$packetSlot = $entry->complexSlotMap->mapCoreToNet($slotId) ?? null;
|
||||
if($packetSlot === null){
|
||||
continue;
|
||||
}
|
||||
unset($this->initiatedSlotChanges[$windowId][$packetSlot]);
|
||||
$this->session->sendDataPacket(InventorySlotPacket::create(
|
||||
$windowId,
|
||||
$packetSlot,
|
||||
ItemStackWrapper::legacy($typeConverter->coreItemStackToNet($inventory->getItem($slotId)))
|
||||
$info
|
||||
));
|
||||
}
|
||||
}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))));
|
||||
$this->session->sendDataPacket(InventoryContentPacket::create($windowId, $contents));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function syncAll() : void{
|
||||
foreach($this->windowMap as $inventory){
|
||||
$this->syncContents($inventory);
|
||||
}
|
||||
foreach($this->complexWindows as $entry){
|
||||
$this->syncContents($entry->getInventory());
|
||||
foreach($this->inventories as $entry){
|
||||
$this->syncContents($entry->inventory);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
public function requestSyncAll() : void{
|
||||
$this->fullSyncRequested = true;
|
||||
}
|
||||
|
||||
if(!$inventory->slotExists($slot)){
|
||||
public function syncMismatchedPredictedSlotChanges() : void{
|
||||
foreach($this->inventories as $entry){
|
||||
$inventory = $entry->inventory;
|
||||
foreach($entry->predictions as $slot => $expectedItem){
|
||||
if(!$inventory->slotExists($slot) || $entry->itemStackInfos[$slot] === null){
|
||||
continue; //TODO: size desync ???
|
||||
}
|
||||
$actualItem = $inventory->getItem($slot);
|
||||
if(!$actualItem->equalsExact($expectedItem)){
|
||||
$this->session->getLogger()->debug("Detected prediction mismatch in inventory " . get_class($inventory) . "#" . spl_object_id($inventory) . " slot $slot");
|
||||
|
||||
//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");
|
||||
$entry->pendingSyncs[$slot] = $slot;
|
||||
}
|
||||
|
||||
$entry->predictions = [];
|
||||
}
|
||||
}
|
||||
|
||||
public function flushPendingUpdates() : void{
|
||||
if($this->fullSyncRequested){
|
||||
$this->fullSyncRequested = false;
|
||||
$this->session->getLogger()->debug("Full inventory sync requested, sending contents of " . count($this->inventories) . " inventories");
|
||||
$this->syncAll();
|
||||
}else{
|
||||
foreach($this->inventories as $entry){
|
||||
if(count($entry->pendingSyncs) === 0){
|
||||
continue;
|
||||
}
|
||||
$inventory = $entry->inventory;
|
||||
$this->session->getLogger()->debug("Syncing slots " . implode(", ", array_keys($entry->pendingSyncs)) . " in inventory " . get_class($inventory) . "#" . spl_object_id($inventory));
|
||||
foreach($entry->pendingSyncs as $slot){
|
||||
$this->syncSlot($inventory, $slot);
|
||||
}
|
||||
$entry->pendingSyncs = [];
|
||||
}
|
||||
}
|
||||
|
||||
$this->initiatedSlotChanges = [];
|
||||
}
|
||||
|
||||
public function syncData(Inventory $inventory, int $propertyId, int $value) : void{
|
||||
@ -453,11 +561,17 @@ class InventoryManager{
|
||||
}
|
||||
|
||||
public function syncSelectedHotbarSlot() : void{
|
||||
$selected = $this->player->getInventory()->getHeldItemIndex();
|
||||
$playerInventory = $this->player->getInventory();
|
||||
$selected = $playerInventory->getHeldItemIndex();
|
||||
if($selected !== $this->clientSelectedHotbarSlot){
|
||||
$itemStackInfo = $this->getItemStackInfo($playerInventory, $selected);
|
||||
if($itemStackInfo === null){
|
||||
throw new AssumptionFailedError("Player inventory slots should always be tracked");
|
||||
}
|
||||
|
||||
$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
|
||||
@ -469,9 +583,37 @@ class InventoryManager{
|
||||
public function syncCreative() : void{
|
||||
$typeConverter = TypeConverter::getInstance();
|
||||
|
||||
$nextEntryId = 1;
|
||||
$this->session->sendDataPacket(CreativeContentPacket::create(array_map(function(Item $item) use($typeConverter, &$nextEntryId) : CreativeContentEntry{
|
||||
return new CreativeContentEntry($nextEntryId++, $typeConverter->coreItemStackToNet($item));
|
||||
}, $this->player->isSpectator() ? [] : CreativeInventory::getInstance()->getAll())));
|
||||
$entries = [];
|
||||
if(!$this->player->isSpectator()){
|
||||
//creative inventory may have holes if items were unregistered - ensure network IDs used are always consistent
|
||||
foreach(CreativeInventory::getInstance()->getAll() as $k => $item){
|
||||
$entries[] = new CreativeContentEntry($k, $typeConverter->coreItemStackToNet($item));
|
||||
}
|
||||
}
|
||||
$this->session->sendDataPacket(CreativeContentPacket::create($entries));
|
||||
}
|
||||
|
||||
private function newItemStackId() : int{
|
||||
return $this->nextItemStackId++;
|
||||
}
|
||||
|
||||
public function getItemStackInfo(Inventory $inventory, int $slot) : ?ItemStackInfo{
|
||||
$entry = $this->inventories[spl_object_id($inventory)] ?? null;
|
||||
return $entry?->itemStackInfos[$slot] ?? null;
|
||||
}
|
||||
|
||||
private function trackItemStack(Inventory $inventory, int $slotId, ItemStack $itemStack, ?int $itemStackRequestId) : ItemStackInfo{
|
||||
$entry = $this->inventories[spl_object_id($inventory)] ?? null;
|
||||
if($entry === null){
|
||||
throw new \LogicException("Cannot track an item stack for an untracked inventory");
|
||||
}
|
||||
$existing = $entry->itemStackInfos[$slotId] ?? null;
|
||||
if($existing !== null && $existing->getItemStack()->equals($itemStack) && $existing->getRequestId() === $itemStackRequestId){
|
||||
return $existing;
|
||||
}
|
||||
|
||||
//TODO: ItemStack->isNull() would be nice to have here
|
||||
$info = new ItemStackInfo($itemStackRequestId, $itemStack->getId() === 0 ? 0 : $this->newItemStackId(), $itemStack);
|
||||
return $entry->itemStackInfos[$slotId] = $info;
|
||||
}
|
||||
}
|
||||
|
52
src/network/mcpe/InventoryManagerEntry.php
Normal file
52
src/network/mcpe/InventoryManagerEntry.php
Normal file
@ -0,0 +1,52 @@
|
||||
<?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\network\mcpe\protocol\types\inventory\ItemStack;
|
||||
|
||||
final class InventoryManagerEntry{
|
||||
/**
|
||||
* @var ItemStack[]
|
||||
* @phpstan-var array<int, ItemStack>
|
||||
*/
|
||||
public array $predictions = [];
|
||||
|
||||
/**
|
||||
* @var ItemStackInfo[]
|
||||
* @phpstan-var array<int, ItemStackInfo>
|
||||
*/
|
||||
public array $itemStackInfos = [];
|
||||
|
||||
/**
|
||||
* @var int[]
|
||||
* @phpstan-var array<int, int>
|
||||
*/
|
||||
public array $pendingSyncs = [];
|
||||
|
||||
public function __construct(
|
||||
public Inventory $inventory,
|
||||
public ?ComplexInventoryMapEntry $complexSlotMap = null
|
||||
){}
|
||||
}
|
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; }
|
||||
}
|
@ -1146,6 +1146,7 @@ class NetworkSession{
|
||||
$attribute->markSynchronized();
|
||||
}
|
||||
}
|
||||
$this->invManager?->flushPendingUpdates();
|
||||
|
||||
$this->flushSendBuffer();
|
||||
}
|
||||
|
23
src/network/mcpe/cache/CraftingDataCache.php
vendored
23
src/network/mcpe/cache/CraftingDataCache.php
vendored
@ -25,6 +25,8 @@ namespace pocketmine\network\mcpe\cache;
|
||||
|
||||
use pocketmine\crafting\CraftingManager;
|
||||
use pocketmine\crafting\FurnaceType;
|
||||
use pocketmine\crafting\ShapedRecipe;
|
||||
use pocketmine\crafting\ShapelessRecipe;
|
||||
use pocketmine\crafting\ShapelessRecipeType;
|
||||
use pocketmine\item\Item;
|
||||
use pocketmine\network\mcpe\convert\ItemTranslator;
|
||||
@ -76,12 +78,12 @@ final class CraftingDataCache{
|
||||
private function buildCraftingDataCache(CraftingManager $manager) : CraftingDataPacket{
|
||||
Timings::$craftingDataCacheRebuild->startTiming();
|
||||
|
||||
$counter = 0;
|
||||
$nullUUID = Uuid::fromString(Uuid::NIL);
|
||||
$converter = TypeConverter::getInstance();
|
||||
$recipesWithTypeIds = [];
|
||||
foreach($manager->getShapelessRecipes() as $list){
|
||||
foreach($list as $recipe){
|
||||
|
||||
foreach($manager->getCraftingRecipeIndex() as $index => $recipe){
|
||||
if($recipe instanceof ShapelessRecipe){
|
||||
$typeTag = match($recipe->getType()->id()){
|
||||
ShapelessRecipeType::CRAFTING()->id() => CraftingRecipeBlockName::CRAFTING_TABLE,
|
||||
ShapelessRecipeType::STONECUTTER()->id() => CraftingRecipeBlockName::STONECUTTER,
|
||||
@ -89,7 +91,7 @@ final class CraftingDataCache{
|
||||
};
|
||||
$recipesWithTypeIds[] = new ProtocolShapelessRecipe(
|
||||
CraftingDataPacket::ENTRY_SHAPELESS,
|
||||
Binary::writeInt(++$counter),
|
||||
Binary::writeInt($index),
|
||||
array_map(function(Item $item) use ($converter) : RecipeIngredient{
|
||||
return $converter->coreItemStackToRecipeIngredient($item);
|
||||
}, $recipe->getIngredientList()),
|
||||
@ -99,12 +101,9 @@ final class CraftingDataCache{
|
||||
$nullUUID,
|
||||
$typeTag,
|
||||
50,
|
||||
$counter
|
||||
$index
|
||||
);
|
||||
}
|
||||
}
|
||||
foreach($manager->getShapedRecipes() as $list){
|
||||
foreach($list as $recipe){
|
||||
}elseif($recipe instanceof ShapedRecipe){
|
||||
$inputs = [];
|
||||
|
||||
for($row = 0, $height = $recipe->getHeight(); $row < $height; ++$row){
|
||||
@ -114,7 +113,7 @@ final class CraftingDataCache{
|
||||
}
|
||||
$recipesWithTypeIds[] = $r = new ProtocolShapedRecipe(
|
||||
CraftingDataPacket::ENTRY_SHAPED,
|
||||
Binary::writeInt(++$counter),
|
||||
Binary::writeInt($index),
|
||||
$inputs,
|
||||
array_map(function(Item $item) use ($converter) : ItemStack{
|
||||
return $converter->coreItemStackToNet($item);
|
||||
@ -122,8 +121,10 @@ final class CraftingDataCache{
|
||||
$nullUUID,
|
||||
CraftingRecipeBlockName::CRAFTING_TABLE,
|
||||
50,
|
||||
$counter
|
||||
$index
|
||||
);
|
||||
}else{
|
||||
//TODO: probably special recipe types
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -24,11 +24,6 @@ declare(strict_types=1);
|
||||
namespace pocketmine\network\mcpe\convert;
|
||||
|
||||
use pocketmine\block\BlockLegacyIds;
|
||||
use pocketmine\inventory\transaction\action\CreateItemAction;
|
||||
use pocketmine\inventory\transaction\action\DestroyItemAction;
|
||||
use pocketmine\inventory\transaction\action\DropItemAction;
|
||||
use pocketmine\inventory\transaction\action\InventoryAction;
|
||||
use pocketmine\inventory\transaction\action\SlotChangeAction;
|
||||
use pocketmine\item\Durable;
|
||||
use pocketmine\item\Item;
|
||||
use pocketmine\item\ItemFactory;
|
||||
@ -37,17 +32,12 @@ use pocketmine\item\VanillaItems;
|
||||
use pocketmine\nbt\NbtException;
|
||||
use pocketmine\nbt\tag\CompoundTag;
|
||||
use pocketmine\nbt\tag\IntTag;
|
||||
use pocketmine\network\mcpe\InventoryManager;
|
||||
use pocketmine\network\mcpe\protocol\types\GameMode as ProtocolGameMode;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\ContainerIds;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\ItemStack;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\NetworkInventoryAction;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\UIInventorySlotOffset;
|
||||
use pocketmine\network\mcpe\protocol\types\recipe\IntIdMetaItemDescriptor;
|
||||
use pocketmine\network\mcpe\protocol\types\recipe\RecipeIngredient;
|
||||
use pocketmine\network\mcpe\protocol\types\recipe\StringIdMetaItemDescriptor;
|
||||
use pocketmine\player\GameMode;
|
||||
use pocketmine\player\Player;
|
||||
use pocketmine\utils\AssumptionFailedError;
|
||||
use pocketmine\utils\SingletonTrait;
|
||||
|
||||
@ -261,60 +251,4 @@ class TypeConverter{
|
||||
throw TypeConversionException::wrap($e, "Bad itemstack NBT data");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws TypeConversionException
|
||||
*/
|
||||
public function createInventoryAction(NetworkInventoryAction $action, Player $player, InventoryManager $inventoryManager) : ?InventoryAction{
|
||||
if($action->oldItem->getItemStack()->equals($action->newItem->getItemStack())){
|
||||
//filter out useless noise in 1.13
|
||||
return null;
|
||||
}
|
||||
try{
|
||||
$old = $this->netItemStackToCore($action->oldItem->getItemStack());
|
||||
}catch(TypeConversionException $e){
|
||||
throw TypeConversionException::wrap($e, "Inventory action: oldItem");
|
||||
}
|
||||
try{
|
||||
$new = $this->netItemStackToCore($action->newItem->getItemStack());
|
||||
}catch(TypeConversionException $e){
|
||||
throw TypeConversionException::wrap($e, "Inventory action: newItem");
|
||||
}
|
||||
switch($action->sourceType){
|
||||
case NetworkInventoryAction::SOURCE_CONTAINER:
|
||||
if($action->windowId === ContainerIds::UI && $action->inventorySlot === UIInventorySlotOffset::CREATED_ITEM_OUTPUT){
|
||||
return null; //useless noise
|
||||
}
|
||||
$located = $inventoryManager->locateWindowAndSlot($action->windowId, $action->inventorySlot);
|
||||
if($located !== null){
|
||||
[$window, $slot] = $located;
|
||||
return new SlotChangeAction($window, $slot, $old, $new);
|
||||
}
|
||||
|
||||
throw new TypeConversionException("No open container with window ID $action->windowId");
|
||||
case NetworkInventoryAction::SOURCE_WORLD:
|
||||
if($action->inventorySlot !== NetworkInventoryAction::ACTION_MAGIC_SLOT_DROP_ITEM){
|
||||
throw new TypeConversionException("Only expecting drop-item world actions from the client!");
|
||||
}
|
||||
|
||||
return new DropItemAction($new);
|
||||
case NetworkInventoryAction::SOURCE_CREATIVE:
|
||||
switch($action->inventorySlot){
|
||||
case NetworkInventoryAction::ACTION_MAGIC_SLOT_CREATIVE_DELETE_ITEM:
|
||||
return new DestroyItemAction($new);
|
||||
case NetworkInventoryAction::ACTION_MAGIC_SLOT_CREATIVE_CREATE_ITEM:
|
||||
return new CreateItemAction($old);
|
||||
default:
|
||||
throw new TypeConversionException("Unexpected creative action type $action->inventorySlot");
|
||||
|
||||
}
|
||||
case NetworkInventoryAction::SOURCE_TODO:
|
||||
//These are used to balance a transaction that involves special actions, like crafting, enchanting, etc.
|
||||
//The vanilla server just accepted these without verifying them. We don't need to care about them since
|
||||
//we verify crafting by checking for imbalances anyway.
|
||||
return null;
|
||||
default:
|
||||
throw new TypeConversionException("Unknown inventory source type $action->sourceType");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -32,10 +32,10 @@ use pocketmine\entity\animation\ConsumingItemAnimation;
|
||||
use pocketmine\entity\Attribute;
|
||||
use pocketmine\entity\InvalidSkinException;
|
||||
use pocketmine\event\player\PlayerEditBookEvent;
|
||||
use pocketmine\inventory\transaction\action\InventoryAction;
|
||||
use pocketmine\inventory\transaction\CraftingTransaction;
|
||||
use pocketmine\inventory\transaction\action\DropItemAction;
|
||||
use pocketmine\inventory\transaction\InventoryTransaction;
|
||||
use pocketmine\inventory\transaction\TransactionException;
|
||||
use pocketmine\inventory\transaction\TransactionBuilder;
|
||||
use pocketmine\inventory\transaction\TransactionCancelledException;
|
||||
use pocketmine\inventory\transaction\TransactionValidationException;
|
||||
use pocketmine\item\VanillaItems;
|
||||
use pocketmine\item\WritableBook;
|
||||
@ -46,7 +46,6 @@ use pocketmine\math\Vector3;
|
||||
use pocketmine\nbt\tag\CompoundTag;
|
||||
use pocketmine\nbt\tag\StringTag;
|
||||
use pocketmine\network\mcpe\convert\SkinAdapterSingleton;
|
||||
use pocketmine\network\mcpe\convert\TypeConversionException;
|
||||
use pocketmine\network\mcpe\convert\TypeConverter;
|
||||
use pocketmine\network\mcpe\InventoryManager;
|
||||
use pocketmine\network\mcpe\NetworkSession;
|
||||
@ -65,6 +64,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;
|
||||
@ -96,7 +97,8 @@ use pocketmine\network\mcpe\protocol\types\inventory\MismatchTransactionData;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\NetworkInventoryAction;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\NormalTransactionData;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\ReleaseItemTransactionData;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\UIInventorySlotOffset;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\stackrequest\ItemStackRequest;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\stackresponse\ItemStackResponse;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\UseItemOnEntityTransactionData;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\UseItemTransactionData;
|
||||
use pocketmine\network\mcpe\protocol\types\PlayerAction;
|
||||
@ -108,17 +110,18 @@ use pocketmine\player\Player;
|
||||
use pocketmine\utils\AssumptionFailedError;
|
||||
use pocketmine\utils\Limits;
|
||||
use pocketmine\utils\TextFormat;
|
||||
use pocketmine\utils\Utils;
|
||||
use pocketmine\world\format\Chunk;
|
||||
use function array_push;
|
||||
use function base64_encode;
|
||||
use function count;
|
||||
use function fmod;
|
||||
use function implode;
|
||||
use function in_array;
|
||||
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;
|
||||
@ -133,9 +136,6 @@ use const JSON_THROW_ON_ERROR;
|
||||
class InGamePacketHandler extends PacketHandler{
|
||||
private const MAX_FORM_RESPONSE_DEPTH = 2; //modal/simple will be 1, custom forms 2 - they will never contain anything other than string|int|float|bool|null
|
||||
|
||||
/** @var CraftingTransaction|null */
|
||||
protected $craftingTransaction = null;
|
||||
|
||||
/** @var float */
|
||||
protected $lastRightClickTime = 0.0;
|
||||
/** @var UseItemTransactionData|null */
|
||||
@ -276,13 +276,22 @@ class InGamePacketHandler extends PacketHandler{
|
||||
if(count($useItemTransaction->getTransactionData()->getActions()) > 100){
|
||||
throw new PacketHandlingException("Too many actions in item use transaction");
|
||||
}
|
||||
$this->inventoryManager->addPredictedSlotChanges($useItemTransaction->getTransactionData()->getActions());
|
||||
|
||||
$this->inventoryManager->setCurrentItemStackRequestId($useItemTransaction->getRequestId());
|
||||
$this->inventoryManager->addRawPredictedSlotChanges($useItemTransaction->getTransactionData()->getActions());
|
||||
if(!$this->handleUseItemTransaction($useItemTransaction->getTransactionData())){
|
||||
$packetHandled = false;
|
||||
$this->session->getLogger()->debug("Unhandled transaction in PlayerAuthInputPacket (type " . $useItemTransaction->getTransactionData()->getActionType() . ")");
|
||||
}else{
|
||||
$this->inventoryManager->syncMismatchedPredictedSlotChanges();
|
||||
}
|
||||
$this->inventoryManager->setCurrentItemStackRequestId(null);
|
||||
}
|
||||
|
||||
$itemStackRequest = $packet->getItemStackRequest();
|
||||
if($itemStackRequest !== null){
|
||||
$result = $this->handleSingleItemStackRequest($itemStackRequest);
|
||||
$this->session->sendDataPacket(ItemStackResponsePacket::create([$result]));
|
||||
}
|
||||
|
||||
return $packetHandled;
|
||||
@ -316,17 +325,18 @@ class InGamePacketHandler extends PacketHandler{
|
||||
public function handleInventoryTransaction(InventoryTransactionPacket $packet) : bool{
|
||||
$result = true;
|
||||
|
||||
if(count($packet->trData->getActions()) > 100){
|
||||
if(count($packet->trData->getActions()) > 50){
|
||||
throw new PacketHandlingException("Too many actions in inventory transaction");
|
||||
}
|
||||
|
||||
$this->inventoryManager->addPredictedSlotChanges($packet->trData->getActions());
|
||||
$this->inventoryManager->setCurrentItemStackRequestId($packet->requestId);
|
||||
$this->inventoryManager->addRawPredictedSlotChanges($packet->trData->getActions());
|
||||
|
||||
if($packet->trData instanceof NormalTransactionData){
|
||||
$result = $this->handleNormalTransaction($packet->trData);
|
||||
$result = $this->handleNormalTransaction($packet->trData, $packet->requestId);
|
||||
}elseif($packet->trData instanceof MismatchTransactionData){
|
||||
$this->session->getLogger()->debug("Mismatch transaction received");
|
||||
$this->inventoryManager->syncAll();
|
||||
$this->inventoryManager->requestSyncAll();
|
||||
$result = true;
|
||||
}elseif($packet->trData instanceof UseItemTransactionData){
|
||||
$result = $this->handleUseItemTransaction($packet->trData);
|
||||
@ -336,96 +346,80 @@ class InGamePacketHandler extends PacketHandler{
|
||||
$result = $this->handleReleaseItemTransaction($packet->trData);
|
||||
}
|
||||
|
||||
if($this->craftingTransaction === null){ //don't sync if we're waiting to complete a crafting transaction
|
||||
$this->inventoryManager->syncMismatchedPredictedSlotChanges();
|
||||
}
|
||||
$this->inventoryManager->syncMismatchedPredictedSlotChanges();
|
||||
$this->inventoryManager->setCurrentItemStackRequestId(null);
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function handleNormalTransaction(NormalTransactionData $data) : bool{
|
||||
/** @var InventoryAction[] $actions */
|
||||
$actions = [];
|
||||
private function executeInventoryTransaction(InventoryTransaction $transaction, int $requestId) : bool{
|
||||
$this->player->setUsingItem(false);
|
||||
|
||||
$isCraftingPart = false;
|
||||
$converter = TypeConverter::getInstance();
|
||||
foreach($data->getActions() as $networkInventoryAction){
|
||||
if(
|
||||
$networkInventoryAction->sourceType === NetworkInventoryAction::SOURCE_TODO || (
|
||||
$this->craftingTransaction !== null &&
|
||||
!$networkInventoryAction->oldItem->getItemStack()->equals($networkInventoryAction->newItem->getItemStack()) &&
|
||||
$networkInventoryAction->sourceType === NetworkInventoryAction::SOURCE_CONTAINER &&
|
||||
$networkInventoryAction->windowId === ContainerIds::UI &&
|
||||
$networkInventoryAction->inventorySlot === UIInventorySlotOffset::CREATED_ITEM_OUTPUT
|
||||
)
|
||||
){
|
||||
$isCraftingPart = true;
|
||||
}
|
||||
$this->inventoryManager->setCurrentItemStackRequestId($requestId);
|
||||
$this->inventoryManager->addTransactionPredictedSlotChanges($transaction);
|
||||
try{
|
||||
$transaction->execute();
|
||||
}catch(TransactionValidationException $e){
|
||||
$this->inventoryManager->requestSyncAll();
|
||||
$logger = $this->session->getLogger();
|
||||
$logger->debug("Invalid inventory transaction $requestId: " . $e->getMessage());
|
||||
|
||||
try{
|
||||
$action = $converter->createInventoryAction($networkInventoryAction, $this->player, $this->inventoryManager);
|
||||
if($action !== null){
|
||||
$actions[] = $action;
|
||||
}
|
||||
}catch(TypeConversionException $e){
|
||||
$this->session->getLogger()->debug("Error unpacking inventory action: " . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}catch(TransactionCancelledException){
|
||||
$this->session->getLogger()->debug("Inventory transaction $requestId cancelled by a plugin");
|
||||
|
||||
if($isCraftingPart){
|
||||
if($this->craftingTransaction === null){
|
||||
//TODO: this might not be crafting if there is a special inventory open (anvil, enchanting, loom etc)
|
||||
$this->craftingTransaction = new CraftingTransaction($this->player, $this->player->getServer()->getCraftingManager(), $actions);
|
||||
}else{
|
||||
foreach($actions as $action){
|
||||
$this->craftingTransaction->addAction($action);
|
||||
}
|
||||
}
|
||||
|
||||
try{
|
||||
$this->craftingTransaction->validate();
|
||||
}catch(TransactionValidationException $e){
|
||||
//transaction is incomplete - crafting transaction comes in lots of little bits, so we have to collect
|
||||
//all of the parts before we can execute it
|
||||
return true;
|
||||
}
|
||||
$this->player->setUsingItem(false);
|
||||
try{
|
||||
$this->craftingTransaction->execute();
|
||||
}catch(TransactionException $e){
|
||||
$this->session->getLogger()->debug("Failed to execute crafting transaction: " . $e->getMessage());
|
||||
return false;
|
||||
}finally{
|
||||
$this->craftingTransaction = null;
|
||||
}
|
||||
}else{
|
||||
//normal transaction fallthru
|
||||
if($this->craftingTransaction !== null){
|
||||
$this->session->getLogger()->debug("Got unexpected normal inventory action with incomplete crafting transaction, refusing to execute crafting");
|
||||
$this->craftingTransaction = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
if(count($actions) === 0){
|
||||
//TODO: 1.13+ often sends transactions with nothing but useless crap in them, no need for the debug noise
|
||||
return true;
|
||||
}
|
||||
|
||||
$this->player->setUsingItem(false);
|
||||
$transaction = new InventoryTransaction($this->player, $actions);
|
||||
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()));
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}finally{
|
||||
$this->inventoryManager->syncMismatchedPredictedSlotChanges();
|
||||
$this->inventoryManager->setCurrentItemStackRequestId(null);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function handleNormalTransaction(NormalTransactionData $data, int $itemStackRequestId) : bool{
|
||||
//When the ItemStackRequest system is used, this transaction type is only used for dropping items by pressing Q.
|
||||
//I don't know why they don't just use ItemStackRequest for that too, which already supports dropping items by
|
||||
//clicking them outside an open inventory menu, but for now it is what it is.
|
||||
//Fortunately, this means we can be extremely strict about the validation criteria.
|
||||
|
||||
if(count($data->getActions()) > 2){
|
||||
throw new PacketHandlingException("Expected exactly 2 actions for dropping an item");
|
||||
}
|
||||
|
||||
foreach($data->getActions() as $networkInventoryAction){
|
||||
if($networkInventoryAction->sourceType === NetworkInventoryAction::SOURCE_WORLD){
|
||||
//drop item - we don't need to validate this, we only care about the count
|
||||
//if the resulting actions don't match the client for some reason, it will trigger an automatic
|
||||
//prediction rollback anyway.
|
||||
//it's technically possible to see this more than once, but a normal client should never do that.
|
||||
$inventory = $this->player->getInventory();
|
||||
|
||||
$heldItemStack = $this->inventoryManager->getItemStackInfo($inventory, $inventory->getHeldItemIndex())?->getItemStack();
|
||||
if($heldItemStack === null){
|
||||
throw new AssumptionFailedError("Missing itemstack info for held item");
|
||||
}
|
||||
$droppedItemStack = $networkInventoryAction->newItem->getItemStack();
|
||||
//because the client doesn't tell us the expected itemstack ID, we have to deep-compare our known
|
||||
//itemstack info with the one the client sent. This is costly, but we don't have any other option :(
|
||||
if(!$heldItemStack->equalsWithoutCount($droppedItemStack) || $heldItemStack->getCount() < $droppedItemStack->getCount()){
|
||||
return false;
|
||||
}
|
||||
|
||||
$newHeldItem = $inventory->getItemInHand();
|
||||
$droppedItem = $newHeldItem->pop($droppedItemStack->getCount());
|
||||
|
||||
$builder = new TransactionBuilder();
|
||||
$builder->getInventory($inventory)->setItem($inventory->getHeldItemIndex(), $newHeldItem);
|
||||
$builder->addAction(new DropItemAction($droppedItem));
|
||||
|
||||
$transaction = new InventoryTransaction($this->player, $builder->generateActions());
|
||||
return $this->executeInventoryTransaction($transaction, $itemStackRequestId);
|
||||
}
|
||||
}
|
||||
|
||||
throw new PacketHandlingException("Legacy 'normal' transactions should only be used for dropping items");
|
||||
}
|
||||
|
||||
private function handleUseItemTransaction(UseItemTransactionData $data) : bool{
|
||||
$this->player->selectHotbarSlot($data->getHotbarSlot());
|
||||
|
||||
@ -537,6 +531,43 @@ class InGamePacketHandler extends PacketHandler{
|
||||
return false;
|
||||
}
|
||||
|
||||
private function handleSingleItemStackRequest(ItemStackRequest $request) : ItemStackResponse{
|
||||
if(count($request->getActions()) > 20){
|
||||
//TODO: we can probably lower this limit, but this will do for now
|
||||
throw new PacketHandlingException("Too many actions in ItemStackRequest");
|
||||
}
|
||||
$executor = new ItemStackRequestExecutor($this->player, $this->inventoryManager, $request);
|
||||
try{
|
||||
$transaction = $executor->generateInventoryTransaction();
|
||||
$result = $this->executeInventoryTransaction($transaction, $request->getRequestId());
|
||||
}catch(ItemStackRequestProcessException $e){
|
||||
$result = false;
|
||||
$this->session->getLogger()->debug("ItemStackRequest #" . $request->getRequestId() . " failed: " . $e->getMessage());
|
||||
$this->session->getLogger()->debug(implode("\n", Utils::printableExceptionInfo($e)));
|
||||
$this->inventoryManager->requestSyncAll();
|
||||
}
|
||||
|
||||
if(!$result){
|
||||
return new ItemStackResponse(ItemStackResponse::RESULT_ERROR, $request->getRequestId());
|
||||
}
|
||||
return $executor->buildItemStackResponse();
|
||||
}
|
||||
|
||||
public function handleItemStackRequest(ItemStackRequestPacket $packet) : bool{
|
||||
$responses = [];
|
||||
if(count($packet->getRequests()) > 80){
|
||||
//TODO: we can probably lower this limit, but this will do for now
|
||||
throw new PacketHandlingException("Too many requests in ItemStackRequestPacket");
|
||||
}
|
||||
foreach($packet->getRequests() as $request){
|
||||
$responses[] = $this->handleSingleItemStackRequest($request);
|
||||
}
|
||||
|
||||
$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
|
||||
|
90
src/network/mcpe/handler/ItemStackContainerIdTranslator.php
Normal file
90
src/network/mcpe/handler/ItemStackContainerIdTranslator.php
Normal file
@ -0,0 +1,90 @@
|
||||
<?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\network\mcpe\protocol\types\inventory\ContainerIds;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\ContainerUIIds;
|
||||
use pocketmine\network\PacketHandlingException;
|
||||
|
||||
final class ItemStackContainerIdTranslator{
|
||||
|
||||
private function __construct(){
|
||||
//NOOP
|
||||
}
|
||||
|
||||
public static function translate(int $containerInterfaceId, int $currentWindowId) : 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 => $currentWindowId,
|
||||
|
||||
//all preview slots are ignored, since the client shouldn't be modifying those directly
|
||||
|
||||
default => throw new PacketHandlingException("Unexpected container UI ID $containerInterfaceId")
|
||||
};
|
||||
}
|
||||
}
|
360
src/network/mcpe/handler/ItemStackRequestExecutor.php
Normal file
360
src/network/mcpe/handler/ItemStackRequestExecutor.php
Normal file
@ -0,0 +1,360 @@
|
||||
<?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\crafting\CraftingGrid;
|
||||
use pocketmine\inventory\CreativeInventory;
|
||||
use pocketmine\inventory\Inventory;
|
||||
use pocketmine\inventory\transaction\action\CreateItemAction;
|
||||
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\network\mcpe\InventoryManager;
|
||||
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\CraftingCreateSpecificResultStackRequestAction;
|
||||
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\CreativeCreateStackRequestAction;
|
||||
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\UIInventorySlotOffset;
|
||||
use pocketmine\player\Player;
|
||||
use pocketmine\utils\AssumptionFailedError;
|
||||
use function array_key_first;
|
||||
use function count;
|
||||
use function spl_object_id;
|
||||
|
||||
final class ItemStackRequestExecutor{
|
||||
private TransactionBuilder $builder;
|
||||
|
||||
/** @var ItemStackRequestSlotInfo[] */
|
||||
private array $requestSlotInfos = [];
|
||||
|
||||
private ?InventoryTransaction $specialTransaction = null;
|
||||
|
||||
/** @var Item[] */
|
||||
private array $craftingResults = [];
|
||||
|
||||
private ?Item $nextCreatedItem = null;
|
||||
private bool $createdItemFromCreativeInventory = false;
|
||||
private int $createdItemsTakenCount = 0;
|
||||
|
||||
public function __construct(
|
||||
private Player $player,
|
||||
private InventoryManager $inventoryManager,
|
||||
private ItemStackRequest $request
|
||||
){
|
||||
$this->builder = new TransactionBuilder();
|
||||
}
|
||||
|
||||
private function prettyInventoryAndSlot(Inventory $inventory, int $slot) : string{
|
||||
if($inventory instanceof TransactionBuilderInventory){
|
||||
$inventory = $inventory->getActualInventory();
|
||||
}
|
||||
return (new \ReflectionClass($inventory))->getShortName() . "#" . spl_object_id($inventory) . ", slot: $slot";
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ItemStackRequestProcessException
|
||||
*/
|
||||
private function matchItemStack(Inventory $inventory, int $slotId, int $clientItemStackId) : void{
|
||||
$info = $this->inventoryManager->getItemStackInfo($inventory, $slotId);
|
||||
if($info === null){
|
||||
throw new AssumptionFailedError("The inventory is tracked and the slot is valid, so this should not be null");
|
||||
}
|
||||
|
||||
if(!($clientItemStackId < 0 ? $info->getRequestId() === $clientItemStackId : $info->getStackId() === $clientItemStackId)){
|
||||
throw new ItemStackRequestProcessException(
|
||||
$this->prettyInventoryAndSlot($inventory, $slotId) . ": " .
|
||||
"Mismatched expected itemstack, " .
|
||||
"client expected: $clientItemStackId, server actual: " . $info->getStackId() . ", last modified by request: " . ($info->getRequestId() ?? "none")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @phpstan-return array{TransactionBuilderInventory, int}
|
||||
*
|
||||
* @throws ItemStackRequestProcessException
|
||||
*/
|
||||
private function getBuilderInventoryAndSlot(ItemStackRequestSlotInfo $info) : array{
|
||||
$windowId = ItemStackContainerIdTranslator::translate($info->getContainerId(), $this->inventoryManager->getCurrentWindowId());
|
||||
$windowAndSlot = $this->inventoryManager->locateWindowAndSlot($windowId, $info->getSlotId());
|
||||
if($windowAndSlot === null){
|
||||
throw new ItemStackRequestProcessException("No open inventory matches container UI ID: " . $info->getContainerId() . ", slot ID: " . $info->getSlotId());
|
||||
}
|
||||
[$inventory, $slot] = $windowAndSlot;
|
||||
if(!$inventory->slotExists($slot)){
|
||||
throw new ItemStackRequestProcessException("No such inventory slot :" . $this->prettyInventoryAndSlot($inventory, $slot));
|
||||
}
|
||||
|
||||
if($info->getStackId() !== $this->request->getRequestId()){ //the itemstack may have been modified by the current request
|
||||
$this->matchItemStack($inventory, $slot, $info->getStackId());
|
||||
}
|
||||
|
||||
return [$this->builder->getInventory($inventory), $slot];
|
||||
}
|
||||
|
||||
private function transferItems(ItemStackRequestSlotInfo $source, ItemStackRequestSlotInfo $destination, int $count) : void{
|
||||
$removed = $this->removeItemFromSlot($source, $count);
|
||||
$this->addItemToSlot($destination, $removed, $count);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deducts items from an inventory slot, returning a stack containing the removed items.
|
||||
* @throws ItemStackRequestProcessException
|
||||
*/
|
||||
private function removeItemFromSlot(ItemStackRequestSlotInfo $slotInfo, int $count) : Item{
|
||||
$this->requestSlotInfos[] = $slotInfo;
|
||||
[$inventory, $slot] = $this->getBuilderInventoryAndSlot($slotInfo);
|
||||
|
||||
$existingItem = $inventory->getItem($slot);
|
||||
if($existingItem->getCount() < $count){
|
||||
throw new ItemStackRequestProcessException($this->prettyInventoryAndSlot($inventory, $slot) . ": Cannot take $count items from a stack of " . $existingItem->getCount());
|
||||
}
|
||||
|
||||
$removed = $existingItem->pop($count);
|
||||
$inventory->setItem($slot, $existingItem);
|
||||
|
||||
return $removed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds items to the target slot, if they are stackable.
|
||||
*/
|
||||
private function addItemToSlot(ItemStackRequestSlotInfo $slotInfo, Item $item, int $count) : void{
|
||||
$this->requestSlotInfos[] = $slotInfo;
|
||||
[$inventory, $slot] = $this->getBuilderInventoryAndSlot($slotInfo);
|
||||
|
||||
$existingItem = $inventory->getItem($slot);
|
||||
if(!$existingItem->isNull() && !$existingItem->canStackWith($item)){
|
||||
throw new ItemStackRequestProcessException($this->prettyInventoryAndSlot($inventory, $slot) . ": Can only add items to an empty slot, or a slot containing the same item");
|
||||
}
|
||||
|
||||
//we can't use the existing item here; it may be an empty stack
|
||||
$newItem = clone $item;
|
||||
$newItem->setCount($existingItem->getCount() + $count);
|
||||
$inventory->setItem($slot, $newItem);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ItemStackRequestProcessException
|
||||
*/
|
||||
private function setNextCreatedItem(?Item $item, bool $creative = false) : void{
|
||||
if($item !== null && $item->isNull()){
|
||||
$item = null;
|
||||
}
|
||||
if($this->nextCreatedItem !== null){
|
||||
//while this is more complicated than simply adding the action when the item is taken, this ensures that
|
||||
//plugins can tell the difference between 1 item that got split into 2 slots, vs 2 separate items.
|
||||
if($this->createdItemFromCreativeInventory && $this->createdItemsTakenCount > 0){
|
||||
$this->nextCreatedItem->setCount($this->createdItemsTakenCount);
|
||||
$this->builder->addAction(new CreateItemAction($this->nextCreatedItem));
|
||||
}elseif($this->createdItemsTakenCount < $this->nextCreatedItem->getCount()){
|
||||
throw new ItemStackRequestProcessException("Not all of the previous created item was taken");
|
||||
}
|
||||
}
|
||||
$this->nextCreatedItem = $item;
|
||||
$this->createdItemFromCreativeInventory = $creative;
|
||||
$this->createdItemsTakenCount = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ItemStackRequestProcessException
|
||||
*/
|
||||
private function beginCrafting(int $recipeId, int $repetitions) : void{
|
||||
if($this->specialTransaction !== null){
|
||||
throw new ItemStackRequestProcessException("Another special transaction is already in progress");
|
||||
}
|
||||
if($repetitions < 1){ //TODO: upper bound?
|
||||
throw new ItemStackRequestProcessException("Cannot craft a recipe less than 1 time");
|
||||
}
|
||||
$craftingManager = $this->player->getServer()->getCraftingManager();
|
||||
$recipe = $craftingManager->getCraftingRecipeFromIndex($recipeId);
|
||||
if($recipe === null){
|
||||
throw new ItemStackRequestProcessException("No such crafting recipe index: $recipeId");
|
||||
}
|
||||
|
||||
$this->specialTransaction = new CraftingTransaction($this->player, $craftingManager, [], $recipe, $repetitions);
|
||||
|
||||
$currentWindow = $this->player->getCurrentWindow();
|
||||
if($currentWindow !== null && !($currentWindow instanceof CraftingGrid)){
|
||||
throw new ItemStackRequestProcessException("Player's current window is not a crafting grid");
|
||||
}
|
||||
$craftingGrid = $currentWindow ?? $this->player->getCraftingGrid();
|
||||
|
||||
$craftingResults = $recipe->getResultsFor($craftingGrid);
|
||||
foreach($craftingResults as $k => $craftingResult){
|
||||
$craftingResult->setCount($craftingResult->getCount() * $repetitions);
|
||||
$this->craftingResults[$k] = $craftingResult;
|
||||
}
|
||||
if(count($this->craftingResults) === 1){
|
||||
//for multi-output recipes, later actions will tell us which result to create and when
|
||||
$this->setNextCreatedItem($this->craftingResults[array_key_first($this->craftingResults)]);
|
||||
}
|
||||
}
|
||||
|
||||
private function takeCreatedItem(ItemStackRequestSlotInfo $destination, int $count) : void{
|
||||
$createdItem = $this->nextCreatedItem;
|
||||
if($createdItem === null){
|
||||
throw new ItemStackRequestProcessException("No created item is waiting to be taken");
|
||||
}
|
||||
|
||||
if(!$this->createdItemFromCreativeInventory){
|
||||
$availableCount = $createdItem->getCount() - $this->createdItemsTakenCount;
|
||||
if($count > $availableCount){
|
||||
throw new ItemStackRequestProcessException("Not enough created items available to be taken (have $availableCount, tried to take $count)");
|
||||
}
|
||||
}
|
||||
|
||||
$this->createdItemsTakenCount += $count;
|
||||
$this->addItemToSlot($destination, $createdItem, $count);
|
||||
if(!$this->createdItemFromCreativeInventory && $this->createdItemsTakenCount >= $createdItem->getCount()){
|
||||
$this->setNextCreatedItem(null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ItemStackRequestProcessException
|
||||
*/
|
||||
private function assertDoingCrafting() : void{
|
||||
if(!$this->specialTransaction instanceof CraftingTransaction){
|
||||
if($this->specialTransaction === null){
|
||||
throw new ItemStackRequestProcessException("Expected CraftRecipe or CraftRecipeAuto action to precede this action");
|
||||
}else{
|
||||
throw new ItemStackRequestProcessException("A different special transaction is already in progress");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ItemStackRequestProcessException
|
||||
*/
|
||||
private function processItemStackRequestAction(ItemStackRequestAction $action) : void{
|
||||
if(
|
||||
$action instanceof TakeStackRequestAction ||
|
||||
$action instanceof PlaceStackRequestAction
|
||||
){
|
||||
$source = $action->getSource();
|
||||
$destination = $action->getDestination();
|
||||
|
||||
if($source->getContainerId() === ContainerUIIds::CREATED_OUTPUT && $source->getSlotId() === UIInventorySlotOffset::CREATED_ITEM_OUTPUT){
|
||||
$this->takeCreatedItem($destination, $action->getCount());
|
||||
}else{
|
||||
$this->transferItems($source, $destination, $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){
|
||||
//TODO: this action has a "randomly" field, I have no idea what it's used for
|
||||
$dropped = $this->removeItemFromSlot($action->getSource(), $action->getCount());
|
||||
$this->builder->addAction(new DropItemAction($dropped));
|
||||
|
||||
}elseif($action instanceof DestroyStackRequestAction){
|
||||
$destroyed = $this->removeItemFromSlot($action->getSource(), $action->getCount());
|
||||
$this->builder->addAction(new DestroyItemAction($destroyed));
|
||||
|
||||
}elseif($action instanceof CreativeCreateStackRequestAction){
|
||||
$item = CreativeInventory::getInstance()->getItem($action->getCreativeItemId());
|
||||
if($item === null){
|
||||
throw new ItemStackRequestProcessException("No such creative item index: " . $action->getCreativeItemId());
|
||||
}
|
||||
|
||||
$this->setNextCreatedItem($item, true);
|
||||
}elseif($action instanceof CraftRecipeStackRequestAction){
|
||||
$this->beginCrafting($action->getRecipeId(), 1);
|
||||
}elseif($action instanceof CraftRecipeAutoStackRequestAction){
|
||||
$this->beginCrafting($action->getRecipeId(), $action->getRepetitions());
|
||||
}elseif($action instanceof CraftingConsumeInputStackRequestAction){
|
||||
$this->assertDoingCrafting();
|
||||
$this->removeItemFromSlot($action->getSource(), $action->getCount()); //output discarded - we allow CraftingTransaction to verify the balance
|
||||
|
||||
}elseif($action instanceof CraftingCreateSpecificResultStackRequestAction){
|
||||
$this->assertDoingCrafting();
|
||||
|
||||
$nextResultItem = $this->craftingResults[$action->getResultIndex()] ?? null;
|
||||
if($nextResultItem === null){
|
||||
throw new ItemStackRequestProcessException("No such crafting result index: " . $action->getResultIndex());
|
||||
}
|
||||
$this->setNextCreatedItem($nextResultItem);
|
||||
}elseif($action instanceof DeprecatedCraftingResultsStackRequestAction){
|
||||
//no obvious use
|
||||
}else{
|
||||
throw new ItemStackRequestProcessException("Unhandled item stack request action");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ItemStackRequestProcessException
|
||||
*/
|
||||
public function generateInventoryTransaction() : InventoryTransaction{
|
||||
foreach($this->request->getActions() as $k => $action){
|
||||
try{
|
||||
$this->processItemStackRequestAction($action);
|
||||
}catch(ItemStackRequestProcessException $e){
|
||||
throw new ItemStackRequestProcessException("Error processing action $k (" . (new \ReflectionClass($action))->getShortName() . "): " . $e->getMessage(), 0, $e);
|
||||
}
|
||||
}
|
||||
$this->setNextCreatedItem(null);
|
||||
$inventoryActions = $this->builder->generateActions();
|
||||
|
||||
$transaction = $this->specialTransaction ?? new InventoryTransaction($this->player);
|
||||
foreach($inventoryActions as $action){
|
||||
$transaction->addAction($action);
|
||||
}
|
||||
|
||||
return $transaction;
|
||||
}
|
||||
|
||||
public function buildItemStackResponse() : ItemStackResponse{
|
||||
$builder = new ItemStackResponseBuilder($this->request->getRequestId(), $this->inventoryManager);
|
||||
foreach($this->requestSlotInfos as $requestInfo){
|
||||
$builder->addSlot($requestInfo->getContainerId(), $requestInfo->getSlotId());
|
||||
}
|
||||
|
||||
return $builder->build();
|
||||
}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* Thrown when an error occurs during processing of an ItemStackRequest.
|
||||
*/
|
||||
final class ItemStackRequestProcessException extends \RuntimeException{
|
||||
|
||||
}
|
112
src/network/mcpe/handler/ItemStackResponseBuilder.php
Normal file
112
src/network/mcpe/handler/ItemStackResponseBuilder.php
Normal file
@ -0,0 +1,112 @@
|
||||
<?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\inventory\Inventory;
|
||||
use pocketmine\network\mcpe\InventoryManager;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\ContainerUIIds;
|
||||
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\utils\AssumptionFailedError;
|
||||
|
||||
final class ItemStackResponseBuilder{
|
||||
|
||||
/**
|
||||
* @var int[][]
|
||||
* @phpstan-var array<int, array<int, int>>
|
||||
*/
|
||||
private array $changedSlots = [];
|
||||
|
||||
public function __construct(
|
||||
private int $requestId,
|
||||
private InventoryManager $inventoryManager
|
||||
){}
|
||||
|
||||
public function addSlot(int $containerInterfaceId, int $slotId) : void{
|
||||
$this->changedSlots[$containerInterfaceId][$slotId] = $slotId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @phpstan-return array{Inventory, int}
|
||||
*/
|
||||
private function getInventoryAndSlot(int $containerInterfaceId, int $slotId) : ?array{
|
||||
$windowId = ItemStackContainerIdTranslator::translate($containerInterfaceId, $this->inventoryManager->getCurrentWindowId());
|
||||
$windowAndSlot = $this->inventoryManager->locateWindowAndSlot($windowId, $slotId);
|
||||
if($windowAndSlot === null){
|
||||
return null;
|
||||
}
|
||||
[$inventory, $slot] = $windowAndSlot;
|
||||
if(!$inventory->slotExists($slot)){
|
||||
return null;
|
||||
}
|
||||
|
||||
return [$inventory, $slot];
|
||||
}
|
||||
|
||||
public function build() : ItemStackResponse{
|
||||
$responseInfosByContainer = [];
|
||||
foreach($this->changedSlots as $containerInterfaceId => $slotIds){
|
||||
if($containerInterfaceId === ContainerUIIds::CREATED_OUTPUT){
|
||||
continue;
|
||||
}
|
||||
foreach($slotIds as $slotId){
|
||||
$inventoryAndSlot = $this->getInventoryAndSlot($containerInterfaceId, $slotId);
|
||||
if($inventoryAndSlot === null){
|
||||
//a plugin may have closed the inventory during an event, or the slot may have been invalid
|
||||
continue;
|
||||
}
|
||||
[$inventory, $slot] = $inventoryAndSlot;
|
||||
|
||||
$itemStackInfo = $this->inventoryManager->getItemStackInfo($inventory, $slot);
|
||||
if($itemStackInfo === null){
|
||||
throw new AssumptionFailedError("ItemStackInfo should never be null for an open inventory");
|
||||
}
|
||||
if($itemStackInfo->getRequestId() !== $this->requestId){
|
||||
//the itemstack may have been synced due to transaction producing results that the client did not
|
||||
//predict correctly, which will wipe out the tracked request ID (intentionally)
|
||||
//TODO: is this the correct behaviour?
|
||||
continue;
|
||||
}
|
||||
$item = $inventory->getItem($slot);
|
||||
|
||||
$responseInfosByContainer[$containerInterfaceId][] = new ItemStackResponseSlotInfo(
|
||||
$slotId,
|
||||
$slotId,
|
||||
$item->getCount(),
|
||||
$itemStackInfo->getStackId(),
|
||||
$item->getCustomName(),
|
||||
0
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$responseContainerInfos = [];
|
||||
foreach($responseInfosByContainer as $containerInterfaceId => $responseInfos){
|
||||
$responseContainerInfos[] = new ItemStackResponseContainerInfo($containerInterfaceId, $responseInfos);
|
||||
}
|
||||
|
||||
return new ItemStackResponse(ItemStackResponse::RESULT_OK, $this->requestId, $responseContainerInfos);
|
||||
}
|
||||
}
|
@ -98,7 +98,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