mirror of
https://github.com/pmmp/PocketMine-MP.git
synced 2025-06-20 10:24:07 +00:00
Merge branch 'item-stack-request' into item-stack-request-pm5
This commit is contained in:
commit
dace20ad1f
@ -46,6 +46,12 @@ class CraftingManager{
|
|||||||
*/
|
*/
|
||||||
protected array $shapelessRecipes = [];
|
protected array $shapelessRecipes = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var CraftingRecipe[]
|
||||||
|
* @phpstan-var array<int, CraftingRecipe>
|
||||||
|
*/
|
||||||
|
private array $craftingRecipeIndex = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var FurnaceRecipeManager[]
|
* @var FurnaceRecipeManager[]
|
||||||
* @phpstan-var array<int, FurnaceRecipeManager>
|
* @phpstan-var array<int, FurnaceRecipeManager>
|
||||||
@ -159,6 +165,14 @@ class CraftingManager{
|
|||||||
return $this->shapedRecipes;
|
return $this->shapedRecipes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return CraftingRecipe[]
|
||||||
|
* @phpstan-return array<int, CraftingRecipe>
|
||||||
|
*/
|
||||||
|
public function getCraftingRecipeIndex() : array{
|
||||||
|
return $this->craftingRecipeIndex;
|
||||||
|
}
|
||||||
|
|
||||||
public function getFurnaceRecipeManager(FurnaceType $furnaceType) : FurnaceRecipeManager{
|
public function getFurnaceRecipeManager(FurnaceType $furnaceType) : FurnaceRecipeManager{
|
||||||
return $this->furnaceRecipeManagers[$furnaceType->id()];
|
return $this->furnaceRecipeManagers[$furnaceType->id()];
|
||||||
}
|
}
|
||||||
@ -181,6 +195,7 @@ class CraftingManager{
|
|||||||
|
|
||||||
public function registerShapedRecipe(ShapedRecipe $recipe) : void{
|
public function registerShapedRecipe(ShapedRecipe $recipe) : void{
|
||||||
$this->shapedRecipes[self::hashOutputs($recipe->getResults())][] = $recipe;
|
$this->shapedRecipes[self::hashOutputs($recipe->getResults())][] = $recipe;
|
||||||
|
$this->craftingRecipeIndex[] = $recipe;
|
||||||
|
|
||||||
foreach($this->recipeRegisteredCallbacks as $callback){
|
foreach($this->recipeRegisteredCallbacks as $callback){
|
||||||
$callback();
|
$callback();
|
||||||
@ -189,6 +204,7 @@ class CraftingManager{
|
|||||||
|
|
||||||
public function registerShapelessRecipe(ShapelessRecipe $recipe) : void{
|
public function registerShapelessRecipe(ShapelessRecipe $recipe) : void{
|
||||||
$this->shapelessRecipes[self::hashOutputs($recipe->getResults())][] = $recipe;
|
$this->shapelessRecipes[self::hashOutputs($recipe->getResults())][] = $recipe;
|
||||||
|
$this->craftingRecipeIndex[] = $recipe;
|
||||||
|
|
||||||
foreach($this->recipeRegisteredCallbacks as $callback){
|
foreach($this->recipeRegisteredCallbacks as $callback){
|
||||||
$callback();
|
$callback();
|
||||||
|
@ -343,7 +343,7 @@ abstract class BaseInventory implements Inventory{
|
|||||||
if($invManager === null){
|
if($invManager === null){
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
$invManager->syncSlot($this, $index);
|
$invManager->onSlotChange($this, $index);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,9 +64,11 @@ class CraftingTransaction extends InventoryTransaction{
|
|||||||
|
|
||||||
private CraftingManager $craftingManager;
|
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);
|
parent::__construct($source, $actions);
|
||||||
$this->craftingManager = $craftingManager;
|
$this->craftingManager = $craftingManager;
|
||||||
|
$this->recipe = $recipe;
|
||||||
|
$this->repetitions = $repetitions;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -225,6 +227,18 @@ class CraftingTransaction extends InventoryTransaction{
|
|||||||
return $iterations;
|
return $iterations;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function validateRecipe(CraftingRecipe $recipe, ?int $expectedRepetitions) : int{
|
||||||
|
//compute number of times recipe was crafted
|
||||||
|
$repetitions = $this->matchOutputs($this->outputs, $recipe->getResultsFor($this->source->getCraftingGrid()));
|
||||||
|
if($expectedRepetitions !== null && $repetitions !== $expectedRepetitions){
|
||||||
|
throw new TransactionValidationException("Expected $expectedRepetitions repetitions, got $repetitions");
|
||||||
|
}
|
||||||
|
//assert that $repetitions x recipe ingredients should be consumed
|
||||||
|
self::matchIngredients($this->inputs, $recipe->getIngredientList(), $repetitions);
|
||||||
|
|
||||||
|
return $repetitions;
|
||||||
|
}
|
||||||
|
|
||||||
public function validate() : void{
|
public function validate() : void{
|
||||||
$this->squashDuplicateSlotChanges();
|
$this->squashDuplicateSlotChanges();
|
||||||
if(count($this->actions) < 1){
|
if(count($this->actions) < 1){
|
||||||
@ -233,6 +247,7 @@ class CraftingTransaction extends InventoryTransaction{
|
|||||||
|
|
||||||
$this->matchItems($this->outputs, $this->inputs);
|
$this->matchItems($this->outputs, $this->inputs);
|
||||||
|
|
||||||
|
if($this->recipe === null){
|
||||||
$failed = 0;
|
$failed = 0;
|
||||||
foreach($this->craftingManager->matchRecipeByOutputs($this->outputs) as $recipe){
|
foreach($this->craftingManager->matchRecipeByOutputs($this->outputs) as $recipe){
|
||||||
try{
|
try{
|
||||||
@ -253,6 +268,9 @@ class CraftingTransaction extends InventoryTransaction{
|
|||||||
if($this->recipe === null){
|
if($this->recipe === null){
|
||||||
throw new TransactionValidationException("Unable to match a recipe to transaction (tried to match against $failed recipes)");
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function callExecuteEvent() : bool{
|
protected function callExecuteEvent() : bool{
|
||||||
|
@ -40,7 +40,6 @@ use pocketmine\inventory\Inventory;
|
|||||||
use pocketmine\inventory\transaction\action\SlotChangeAction;
|
use pocketmine\inventory\transaction\action\SlotChangeAction;
|
||||||
use pocketmine\inventory\transaction\InventoryTransaction;
|
use pocketmine\inventory\transaction\InventoryTransaction;
|
||||||
use pocketmine\item\Item;
|
use pocketmine\item\Item;
|
||||||
use pocketmine\network\mcpe\convert\TypeConversionException;
|
|
||||||
use pocketmine\network\mcpe\convert\TypeConverter;
|
use pocketmine\network\mcpe\convert\TypeConverter;
|
||||||
use pocketmine\network\mcpe\protocol\ClientboundPacket;
|
use pocketmine\network\mcpe\protocol\ClientboundPacket;
|
||||||
use pocketmine\network\mcpe\protocol\ContainerClosePacket;
|
use pocketmine\network\mcpe\protocol\ContainerClosePacket;
|
||||||
@ -53,6 +52,7 @@ use pocketmine\network\mcpe\protocol\MobEquipmentPacket;
|
|||||||
use pocketmine\network\mcpe\protocol\types\BlockPosition;
|
use pocketmine\network\mcpe\protocol\types\BlockPosition;
|
||||||
use pocketmine\network\mcpe\protocol\types\inventory\ContainerIds;
|
use pocketmine\network\mcpe\protocol\types\inventory\ContainerIds;
|
||||||
use pocketmine\network\mcpe\protocol\types\inventory\CreativeContentEntry;
|
use pocketmine\network\mcpe\protocol\types\inventory\CreativeContentEntry;
|
||||||
|
use pocketmine\network\mcpe\protocol\types\inventory\ItemStack;
|
||||||
use pocketmine\network\mcpe\protocol\types\inventory\ItemStackWrapper;
|
use pocketmine\network\mcpe\protocol\types\inventory\ItemStackWrapper;
|
||||||
use pocketmine\network\mcpe\protocol\types\inventory\NetworkInventoryAction;
|
use pocketmine\network\mcpe\protocol\types\inventory\NetworkInventoryAction;
|
||||||
use pocketmine\network\mcpe\protocol\types\inventory\UIInventorySlotOffset;
|
use pocketmine\network\mcpe\protocol\types\inventory\UIInventorySlotOffset;
|
||||||
@ -61,7 +61,6 @@ use pocketmine\network\PacketHandlingException;
|
|||||||
use pocketmine\player\Player;
|
use pocketmine\player\Player;
|
||||||
use pocketmine\utils\AssumptionFailedError;
|
use pocketmine\utils\AssumptionFailedError;
|
||||||
use pocketmine\utils\ObjectSet;
|
use pocketmine\utils\ObjectSet;
|
||||||
use function array_map;
|
|
||||||
use function array_search;
|
use function array_search;
|
||||||
use function get_class;
|
use function get_class;
|
||||||
use function is_int;
|
use function is_int;
|
||||||
@ -89,7 +88,7 @@ class InventoryManager{
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @var Item[][]
|
* @var Item[][]
|
||||||
* @phpstan-var array<int, array<int, Item>>
|
* @phpstan-var array<int, InventoryManagerPredictedChanges>
|
||||||
*/
|
*/
|
||||||
private array $initiatedSlotChanges = [];
|
private array $initiatedSlotChanges = [];
|
||||||
private int $clientSelectedHotbarSlot = -1;
|
private int $clientSelectedHotbarSlot = -1;
|
||||||
@ -101,6 +100,15 @@ class InventoryManager{
|
|||||||
/** @phpstan-var \Closure() : void */
|
/** @phpstan-var \Closure() : void */
|
||||||
private ?\Closure $pendingOpenWindowCallback = null;
|
private ?\Closure $pendingOpenWindowCallback = null;
|
||||||
|
|
||||||
|
private int $nextItemStackId = 1;
|
||||||
|
private ?int $currentItemStackRequestId = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var int[][]
|
||||||
|
* @phpstan-var array<int, array<int, ItemStackInfo>>
|
||||||
|
*/
|
||||||
|
private array $itemStackInfos = [];
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private Player $player,
|
private Player $player,
|
||||||
private NetworkSession $session
|
private NetworkSession $session
|
||||||
@ -143,14 +151,17 @@ class InventoryManager{
|
|||||||
|
|
||||||
private function remove(int $id) : void{
|
private function remove(int $id) : void{
|
||||||
$inventory = $this->windowMap[$id];
|
$inventory = $this->windowMap[$id];
|
||||||
|
unset($this->windowMap[$id]);
|
||||||
|
if($this->getWindowId($inventory) === null){
|
||||||
$splObjectId = spl_object_id($inventory);
|
$splObjectId = spl_object_id($inventory);
|
||||||
unset($this->windowMap[$id], $this->initiatedSlotChanges[$id], $this->complexWindows[$splObjectId]);
|
unset($this->initiatedSlotChanges[$splObjectId], $this->itemStackInfos[$splObjectId], $this->complexWindows[$splObjectId]);
|
||||||
foreach($this->complexSlotToWindowMap as $netSlot => $entry){
|
foreach($this->complexSlotToWindowMap as $netSlot => $entry){
|
||||||
if($entry->getInventory() === $inventory){
|
if($entry->getInventory() === $inventory){
|
||||||
unset($this->complexSlotToWindowMap[$netSlot]);
|
unset($this->complexSlotToWindowMap[$netSlot]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public function getWindowId(Inventory $inventory) : ?int{
|
public function getWindowId(Inventory $inventory) : ?int{
|
||||||
return ($id = array_search($inventory, $this->windowMap, true)) !== false ? $id : null;
|
return ($id = array_search($inventory, $this->windowMap, true)) !== false ? $id : null;
|
||||||
@ -161,7 +172,7 @@ class InventoryManager{
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @phpstan-return array{Inventory, int}
|
* @phpstan-return array{Inventory, int}|null
|
||||||
*/
|
*/
|
||||||
public function locateWindowAndSlot(int $windowId, int $netSlotId) : ?array{
|
public function locateWindowAndSlot(int $windowId, int $netSlotId) : ?array{
|
||||||
if($windowId === ContainerIds::UI){
|
if($windowId === ContainerIds::UI){
|
||||||
@ -178,11 +189,17 @@ class InventoryManager{
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function onTransactionStart(InventoryTransaction $tx) : void{
|
private function addPredictedSlotChange(Inventory $inventory, int $slot, ItemStack $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){
|
foreach($tx->getActions() as $action){
|
||||||
if($action instanceof SlotChangeAction && ($windowId = $this->getWindowId($action->getInventory())) !== null){
|
if($action instanceof SlotChangeAction){
|
||||||
//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
|
//TODO: ItemStackRequestExecutor can probably build these predictions with much lower overhead
|
||||||
$this->initiatedSlotChanges[$windowId][$action->getSlot()] = $action->getTargetItem();
|
$itemStack = TypeConverter::getInstance()->coreItemStackToNet($action->getTargetItem());
|
||||||
|
$this->addPredictedSlotChange($action->getInventory(), $action->getSlot(), $itemStack);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -191,20 +208,32 @@ class InventoryManager{
|
|||||||
* @param NetworkInventoryAction[] $networkInventoryActions
|
* @param NetworkInventoryAction[] $networkInventoryActions
|
||||||
* @throws PacketHandlingException
|
* @throws PacketHandlingException
|
||||||
*/
|
*/
|
||||||
public function addPredictedSlotChanges(array $networkInventoryActions) : void{
|
public function addRawPredictedSlotChanges(array $networkInventoryActions) : void{
|
||||||
foreach($networkInventoryActions as $action){
|
foreach($networkInventoryActions as $action){
|
||||||
if($action->sourceType === NetworkInventoryAction::SOURCE_CONTAINER && (
|
if($action->sourceType !== NetworkInventoryAction::SOURCE_CONTAINER){
|
||||||
isset($this->windowMap[$action->windowId]) ||
|
continue;
|
||||||
($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;
|
|
||||||
|
//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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -355,20 +384,36 @@ class InventoryManager{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function onSlotChange(Inventory $inventory, int $slot) : void{
|
||||||
|
$currentItem = TypeConverter::getInstance()->coreItemStackToNet($inventory->getItem($slot));
|
||||||
|
$predictions = $this->initiatedSlotChanges[spl_object_id($inventory)] ?? null;
|
||||||
|
$clientSideItem = $predictions?->getSlot($slot);
|
||||||
|
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);
|
||||||
|
$this->syncSlot($inventory, $slot);
|
||||||
|
}else{
|
||||||
|
//correctly predicted - associate the change with the currently active itemstack request
|
||||||
|
$this->trackItemStack($inventory, $slot, $currentItem, $this->currentItemStackRequestId);
|
||||||
|
}
|
||||||
|
$predictions?->remove($slot);
|
||||||
|
}
|
||||||
|
|
||||||
public function syncSlot(Inventory $inventory, int $slot) : void{
|
public function syncSlot(Inventory $inventory, int $slot) : void{
|
||||||
|
$itemStackInfo = $this->getItemStackInfo($inventory, $slot);
|
||||||
|
if($itemStackInfo === null){
|
||||||
|
throw new \LogicException("Cannot sync an untracked inventory slot");
|
||||||
|
}
|
||||||
$slotMap = $this->complexWindows[spl_object_id($inventory)] ?? null;
|
$slotMap = $this->complexWindows[spl_object_id($inventory)] ?? null;
|
||||||
if($slotMap !== null){
|
if($slotMap !== null){
|
||||||
$windowId = ContainerIds::UI;
|
$windowId = ContainerIds::UI;
|
||||||
$netSlot = $slotMap->mapCoreToNet($slot) ?? null;
|
$netSlot = $slotMap->mapCoreToNet($slot) ?? throw new AssumptionFailedError("We already have an ItemStackInfo, so this should not be null");
|
||||||
}else{
|
}else{
|
||||||
$windowId = $this->getWindowId($inventory);
|
$windowId = $this->getWindowId($inventory) ?? throw new AssumptionFailedError("We already have an ItemStackInfo, so this should not be null");
|
||||||
$netSlot = $slot;
|
$netSlot = $slot;
|
||||||
}
|
}
|
||||||
if($windowId !== null && $netSlot !== null){
|
|
||||||
$currentItem = $inventory->getItem($slot);
|
$itemStackWrapper = new ItemStackWrapper($itemStackInfo->getStackId(), $itemStackInfo->getItemStack());
|
||||||
$clientSideItem = $this->initiatedSlotChanges[$windowId][$netSlot] ?? null;
|
|
||||||
if($clientSideItem === null || !$clientSideItem->equalsExact($currentItem)){
|
|
||||||
$itemStackWrapper = ItemStackWrapper::legacy(TypeConverter::getInstance()->coreItemStackToNet($currentItem));
|
|
||||||
if($windowId === ContainerIds::OFFHAND){
|
if($windowId === ContainerIds::OFFHAND){
|
||||||
//TODO: HACK!
|
//TODO: HACK!
|
||||||
//The client may sometimes ignore the InventorySlotPacket for the offhand slot.
|
//The client may sometimes ignore the InventorySlotPacket for the offhand slot.
|
||||||
@ -377,11 +422,25 @@ class InventoryManager{
|
|||||||
//BDS (Bedrock Dedicated Server) also seems to work this way.
|
//BDS (Bedrock Dedicated Server) also seems to work this way.
|
||||||
$this->session->sendDataPacket(InventoryContentPacket::create($windowId, [$itemStackWrapper]));
|
$this->session->sendDataPacket(InventoryContentPacket::create($windowId, [$itemStackWrapper]));
|
||||||
}else{
|
}else{
|
||||||
|
if($this->currentItemStackRequestId !== null){
|
||||||
|
//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())));
|
||||||
|
}
|
||||||
$this->session->sendDataPacket(InventorySlotPacket::create($windowId, $netSlot, $itemStackWrapper));
|
$this->session->sendDataPacket(InventorySlotPacket::create($windowId, $netSlot, $itemStackWrapper));
|
||||||
}
|
}
|
||||||
}
|
$predictions = $this->initiatedSlotChanges[spl_object_id($inventory)] ?? null;
|
||||||
unset($this->initiatedSlotChanges[$windowId][$netSlot]);
|
$predictions?->remove($slot);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function syncContents(Inventory $inventory) : void{
|
public function syncContents(Inventory $inventory) : void{
|
||||||
@ -391,26 +450,28 @@ class InventoryManager{
|
|||||||
}else{
|
}else{
|
||||||
$windowId = $this->getWindowId($inventory);
|
$windowId = $this->getWindowId($inventory);
|
||||||
}
|
}
|
||||||
$typeConverter = TypeConverter::getInstance();
|
|
||||||
if($windowId !== null){
|
if($windowId !== null){
|
||||||
|
unset($this->initiatedSlotChanges[spl_object_id($inventory)]);
|
||||||
|
$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($slotMap !== null){
|
if($slotMap !== null){
|
||||||
foreach($inventory->getContents(true) as $slotId => $item){
|
foreach($contents as $slotId => $info){
|
||||||
$packetSlot = $slotMap->mapCoreToNet($slotId) ?? null;
|
$packetSlot = $slotMap->mapCoreToNet($slotId) ?? null;
|
||||||
if($packetSlot === null){
|
if($packetSlot === null){
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
unset($this->initiatedSlotChanges[$windowId][$packetSlot]);
|
|
||||||
$this->session->sendDataPacket(InventorySlotPacket::create(
|
$this->session->sendDataPacket(InventorySlotPacket::create(
|
||||||
$windowId,
|
$windowId,
|
||||||
$packetSlot,
|
$packetSlot,
|
||||||
ItemStackWrapper::legacy($typeConverter->coreItemStackToNet($inventory->getItem($slotId)))
|
$info
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}else{
|
}else{
|
||||||
unset($this->initiatedSlotChanges[$windowId]);
|
$this->session->sendDataPacket(InventoryContentPacket::create($windowId, $contents));
|
||||||
$this->session->sendDataPacket(InventoryContentPacket::create($windowId, array_map(function(Item $itemStack) use ($typeConverter) : ItemStackWrapper{
|
|
||||||
return ItemStackWrapper::legacy($typeConverter->coreItemStackToNet($itemStack));
|
|
||||||
}, $inventory->getContents(true))));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -425,24 +486,18 @@ class InventoryManager{
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function syncMismatchedPredictedSlotChanges() : void{
|
public function syncMismatchedPredictedSlotChanges() : void{
|
||||||
foreach($this->initiatedSlotChanges as $windowId => $slots){
|
foreach($this->initiatedSlotChanges as $predictions){
|
||||||
foreach($slots as $netSlot => $expectedItem){
|
$inventory = $predictions->getInventory();
|
||||||
$located = $this->locateWindowAndSlot($windowId, $netSlot);
|
foreach($predictions->getSlots() as $slot => $expectedItem){
|
||||||
if($located === null){
|
if(!$inventory->slotExists($slot) || $this->getItemStackInfo($inventory, $slot) === null){
|
||||||
continue;
|
|
||||||
}
|
|
||||||
[$inventory, $slot] = $located;
|
|
||||||
|
|
||||||
if(!$inventory->slotExists($slot)){
|
|
||||||
continue; //TODO: size desync ???
|
continue; //TODO: size desync ???
|
||||||
}
|
}
|
||||||
$actualItem = $inventory->getItem($slot);
|
|
||||||
if(!$actualItem->equalsExact($expectedItem)){
|
//any prediction that still exists at this point is a slot that was predicted to change but didn't
|
||||||
$this->session->getLogger()->debug("Detected prediction mismatch in inventory " . get_class($inventory) . "#" . spl_object_id($inventory) . " slot $slot");
|
$this->session->getLogger()->debug("Detected prediction mismatch in inventory " . get_class($inventory) . "#" . spl_object_id($inventory) . " slot $slot");
|
||||||
$this->syncSlot($inventory, $slot);
|
$this->syncSlot($inventory, $slot);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
$this->initiatedSlotChanges = [];
|
$this->initiatedSlotChanges = [];
|
||||||
}
|
}
|
||||||
@ -459,11 +514,14 @@ class InventoryManager{
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function syncSelectedHotbarSlot() : void{
|
public function syncSelectedHotbarSlot() : void{
|
||||||
$selected = $this->player->getInventory()->getHeldItemIndex();
|
$playerInventory = $this->player->getInventory();
|
||||||
|
$selected = $playerInventory->getHeldItemIndex();
|
||||||
if($selected !== $this->clientSelectedHotbarSlot){
|
if($selected !== $this->clientSelectedHotbarSlot){
|
||||||
|
$itemStackInfo = $this->itemStackInfos[spl_object_id($playerInventory)][$selected];
|
||||||
|
|
||||||
$this->session->sendDataPacket(MobEquipmentPacket::create(
|
$this->session->sendDataPacket(MobEquipmentPacket::create(
|
||||||
$this->player->getId(),
|
$this->player->getId(),
|
||||||
ItemStackWrapper::legacy(TypeConverter::getInstance()->coreItemStackToNet($this->player->getInventory()->getItemInHand())),
|
new ItemStackWrapper($itemStackInfo->getStackId(), $itemStackInfo->getItemStack()),
|
||||||
$selected,
|
$selected,
|
||||||
$selected,
|
$selected,
|
||||||
ContainerIds::INVENTORY
|
ContainerIds::INVENTORY
|
||||||
@ -475,9 +533,55 @@ class InventoryManager{
|
|||||||
public function syncCreative() : void{
|
public function syncCreative() : void{
|
||||||
$typeConverter = TypeConverter::getInstance();
|
$typeConverter = TypeConverter::getInstance();
|
||||||
|
|
||||||
$nextEntryId = 1;
|
$entries = [];
|
||||||
$this->session->sendDataPacket(CreativeContentPacket::create(array_map(function(Item $item) use($typeConverter, &$nextEntryId) : CreativeContentEntry{
|
if(!$this->player->isSpectator()){
|
||||||
return new CreativeContentEntry($nextEntryId++, $typeConverter->coreItemStackToNet($item));
|
//creative inventory may have holes if items were unregistered - ensure network IDs used are always consistent
|
||||||
}, $this->player->isSpectator() ? [] : CreativeInventory::getInstance()->getAll())));
|
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{
|
||||||
|
return $this->itemStackInfos[spl_object_id($inventory)][$slot] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function trackItemStack(Inventory $inventory, int $slotId, ItemStack $itemStack, ?int $itemStackRequestId) : ItemStackInfo{
|
||||||
|
$existing = $this->itemStackInfos[spl_object_id($inventory)][$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 $this->itemStackInfos[spl_object_id($inventory)][$slotId] = $info;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function matchItemStack(Inventory $inventory, int $slotId, int $clientItemStackId) : 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(!($clientItemStackId < 0 ? $info->getRequestId() === $clientItemStackId : $info->getStackId() === $clientItemStackId)){
|
||||||
|
$this->session->getLogger()->debug(
|
||||||
|
"Mismatched expected itemstack: " . get_class($inventory) . "#" . $inventoryObjectId . ", " .
|
||||||
|
"slot: $slotId, client expected: $clientItemStackId, server 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\network\mcpe\protocol\types\inventory\ItemStack;
|
||||||
|
|
||||||
|
final class InventoryManagerPredictedChanges{
|
||||||
|
/**
|
||||||
|
* @var ItemStack[]
|
||||||
|
* @phpstan-var array<int, ItemStack>
|
||||||
|
*/
|
||||||
|
private array $slots = [];
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private Inventory $inventory
|
||||||
|
){}
|
||||||
|
|
||||||
|
public function getInventory() : Inventory{ return $this->inventory; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return ItemStack[]
|
||||||
|
* @phpstan-return array<int, ItemStack>
|
||||||
|
*/
|
||||||
|
public function getSlots() : array{
|
||||||
|
return $this->slots;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSlot(int $slot) : ?ItemStack{
|
||||||
|
return $this->slots[$slot] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function add(int $slot, ItemStack $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; }
|
||||||
|
}
|
@ -449,19 +449,19 @@ class NetworkSession{
|
|||||||
$decodeTimings->stopTiming();
|
$decodeTimings->stopTiming();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$ev = new DataPacketReceiveEvent($this, $packet);
|
||||||
|
$ev->call();
|
||||||
|
if(!$ev->isCancelled()){
|
||||||
$handlerTimings = Timings::getHandleDataPacketTimings($packet);
|
$handlerTimings = Timings::getHandleDataPacketTimings($packet);
|
||||||
$handlerTimings->startTiming();
|
$handlerTimings->startTiming();
|
||||||
try{
|
try{
|
||||||
//TODO: I'm not sure DataPacketReceiveEvent should be included in the handler timings, but it needs to be
|
if($this->handler === null || !$packet->handle($this->handler)){
|
||||||
//included for now to ensure the receivePacket timings are counted the way they were before
|
|
||||||
$ev = new DataPacketReceiveEvent($this, $packet);
|
|
||||||
$ev->call();
|
|
||||||
if(!$ev->isCancelled() && ($this->handler === null || !$packet->handle($this->handler))){
|
|
||||||
$this->logger->debug("Unhandled " . $packet->getName() . ": " . base64_encode($stream->getBuffer()));
|
$this->logger->debug("Unhandled " . $packet->getName() . ": " . base64_encode($stream->getBuffer()));
|
||||||
}
|
}
|
||||||
}finally{
|
}finally{
|
||||||
$handlerTimings->stopTiming();
|
$handlerTimings->stopTiming();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}finally{
|
}finally{
|
||||||
$timings->stopTiming();
|
$timings->stopTiming();
|
||||||
}
|
}
|
||||||
|
23
src/network/mcpe/cache/CraftingDataCache.php
vendored
23
src/network/mcpe/cache/CraftingDataCache.php
vendored
@ -26,6 +26,8 @@ namespace pocketmine\network\mcpe\cache;
|
|||||||
use pocketmine\crafting\CraftingManager;
|
use pocketmine\crafting\CraftingManager;
|
||||||
use pocketmine\crafting\FurnaceType;
|
use pocketmine\crafting\FurnaceType;
|
||||||
use pocketmine\crafting\RecipeIngredient;
|
use pocketmine\crafting\RecipeIngredient;
|
||||||
|
use pocketmine\crafting\ShapedRecipe;
|
||||||
|
use pocketmine\crafting\ShapelessRecipe;
|
||||||
use pocketmine\crafting\ShapelessRecipeType;
|
use pocketmine\crafting\ShapelessRecipeType;
|
||||||
use pocketmine\item\Item;
|
use pocketmine\item\Item;
|
||||||
use pocketmine\network\mcpe\convert\GlobalItemTypeDictionary;
|
use pocketmine\network\mcpe\convert\GlobalItemTypeDictionary;
|
||||||
@ -78,12 +80,12 @@ final class CraftingDataCache{
|
|||||||
private function buildCraftingDataCache(CraftingManager $manager) : CraftingDataPacket{
|
private function buildCraftingDataCache(CraftingManager $manager) : CraftingDataPacket{
|
||||||
Timings::$craftingDataCacheRebuild->startTiming();
|
Timings::$craftingDataCacheRebuild->startTiming();
|
||||||
|
|
||||||
$counter = 0;
|
|
||||||
$nullUUID = Uuid::fromString(Uuid::NIL);
|
$nullUUID = Uuid::fromString(Uuid::NIL);
|
||||||
$converter = TypeConverter::getInstance();
|
$converter = TypeConverter::getInstance();
|
||||||
$recipesWithTypeIds = [];
|
$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()){
|
$typeTag = match($recipe->getType()->id()){
|
||||||
ShapelessRecipeType::CRAFTING()->id() => CraftingRecipeBlockName::CRAFTING_TABLE,
|
ShapelessRecipeType::CRAFTING()->id() => CraftingRecipeBlockName::CRAFTING_TABLE,
|
||||||
ShapelessRecipeType::STONECUTTER()->id() => CraftingRecipeBlockName::STONECUTTER,
|
ShapelessRecipeType::STONECUTTER()->id() => CraftingRecipeBlockName::STONECUTTER,
|
||||||
@ -93,7 +95,7 @@ final class CraftingDataCache{
|
|||||||
};
|
};
|
||||||
$recipesWithTypeIds[] = new ProtocolShapelessRecipe(
|
$recipesWithTypeIds[] = new ProtocolShapelessRecipe(
|
||||||
CraftingDataPacket::ENTRY_SHAPELESS,
|
CraftingDataPacket::ENTRY_SHAPELESS,
|
||||||
Binary::writeInt(++$counter),
|
Binary::writeInt($index),
|
||||||
array_map(function(RecipeIngredient $item) use ($converter) : ProtocolRecipeIngredient{
|
array_map(function(RecipeIngredient $item) use ($converter) : ProtocolRecipeIngredient{
|
||||||
return $converter->coreRecipeIngredientToNet($item);
|
return $converter->coreRecipeIngredientToNet($item);
|
||||||
}, $recipe->getIngredientList()),
|
}, $recipe->getIngredientList()),
|
||||||
@ -103,12 +105,9 @@ final class CraftingDataCache{
|
|||||||
$nullUUID,
|
$nullUUID,
|
||||||
$typeTag,
|
$typeTag,
|
||||||
50,
|
50,
|
||||||
$counter
|
$index
|
||||||
);
|
);
|
||||||
}
|
}elseif($recipe instanceof ShapedRecipe){
|
||||||
}
|
|
||||||
foreach($manager->getShapedRecipes() as $list){
|
|
||||||
foreach($list as $recipe){
|
|
||||||
$inputs = [];
|
$inputs = [];
|
||||||
|
|
||||||
for($row = 0, $height = $recipe->getHeight(); $row < $height; ++$row){
|
for($row = 0, $height = $recipe->getHeight(); $row < $height; ++$row){
|
||||||
@ -118,7 +117,7 @@ final class CraftingDataCache{
|
|||||||
}
|
}
|
||||||
$recipesWithTypeIds[] = $r = new ProtocolShapedRecipe(
|
$recipesWithTypeIds[] = $r = new ProtocolShapedRecipe(
|
||||||
CraftingDataPacket::ENTRY_SHAPED,
|
CraftingDataPacket::ENTRY_SHAPED,
|
||||||
Binary::writeInt(++$counter),
|
Binary::writeInt($index),
|
||||||
$inputs,
|
$inputs,
|
||||||
array_map(function(Item $item) use ($converter) : ItemStack{
|
array_map(function(Item $item) use ($converter) : ItemStack{
|
||||||
return $converter->coreItemStackToNet($item);
|
return $converter->coreItemStackToNet($item);
|
||||||
@ -126,8 +125,10 @@ final class CraftingDataCache{
|
|||||||
$nullUUID,
|
$nullUUID,
|
||||||
CraftingRecipeBlockName::CRAFTING_TABLE,
|
CraftingRecipeBlockName::CRAFTING_TABLE,
|
||||||
50,
|
50,
|
||||||
$counter
|
$index
|
||||||
);
|
);
|
||||||
|
}else{
|
||||||
|
//TODO: probably special recipe types
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,27 +29,17 @@ use pocketmine\crafting\MetaWildcardRecipeIngredient;
|
|||||||
use pocketmine\crafting\RecipeIngredient;
|
use pocketmine\crafting\RecipeIngredient;
|
||||||
use pocketmine\crafting\TagWildcardRecipeIngredient;
|
use pocketmine\crafting\TagWildcardRecipeIngredient;
|
||||||
use pocketmine\data\bedrock\item\BlockItemIdMap;
|
use pocketmine\data\bedrock\item\BlockItemIdMap;
|
||||||
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\Item;
|
use pocketmine\item\Item;
|
||||||
use pocketmine\item\VanillaItems;
|
use pocketmine\item\VanillaItems;
|
||||||
use pocketmine\nbt\NbtException;
|
use pocketmine\nbt\NbtException;
|
||||||
use pocketmine\nbt\tag\CompoundTag;
|
use pocketmine\nbt\tag\CompoundTag;
|
||||||
use pocketmine\network\mcpe\InventoryManager;
|
|
||||||
use pocketmine\network\mcpe\protocol\types\GameMode as ProtocolGameMode;
|
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\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\IntIdMetaItemDescriptor;
|
||||||
use pocketmine\network\mcpe\protocol\types\recipe\RecipeIngredient as ProtocolRecipeIngredient;
|
use pocketmine\network\mcpe\protocol\types\recipe\RecipeIngredient as ProtocolRecipeIngredient;
|
||||||
use pocketmine\network\mcpe\protocol\types\recipe\StringIdMetaItemDescriptor;
|
use pocketmine\network\mcpe\protocol\types\recipe\StringIdMetaItemDescriptor;
|
||||||
use pocketmine\network\mcpe\protocol\types\recipe\TagItemDescriptor;
|
use pocketmine\network\mcpe\protocol\types\recipe\TagItemDescriptor;
|
||||||
use pocketmine\player\GameMode;
|
use pocketmine\player\GameMode;
|
||||||
use pocketmine\player\Player;
|
|
||||||
use pocketmine\utils\AssumptionFailedError;
|
use pocketmine\utils\AssumptionFailedError;
|
||||||
use pocketmine\utils\SingletonTrait;
|
use pocketmine\utils\SingletonTrait;
|
||||||
use function get_class;
|
use function get_class;
|
||||||
@ -239,60 +229,4 @@ class TypeConverter{
|
|||||||
|
|
||||||
return $itemResult;
|
return $itemResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @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,11 +32,11 @@ use pocketmine\entity\animation\ConsumingItemAnimation;
|
|||||||
use pocketmine\entity\Attribute;
|
use pocketmine\entity\Attribute;
|
||||||
use pocketmine\entity\InvalidSkinException;
|
use pocketmine\entity\InvalidSkinException;
|
||||||
use pocketmine\event\player\PlayerEditBookEvent;
|
use pocketmine\event\player\PlayerEditBookEvent;
|
||||||
use pocketmine\inventory\transaction\action\InventoryAction;
|
use pocketmine\inventory\transaction\action\DropItemAction;
|
||||||
use pocketmine\inventory\transaction\CraftingTransaction;
|
use pocketmine\inventory\transaction\CraftingTransaction;
|
||||||
use pocketmine\inventory\transaction\InventoryTransaction;
|
use pocketmine\inventory\transaction\InventoryTransaction;
|
||||||
|
use pocketmine\inventory\transaction\TransactionBuilder;
|
||||||
use pocketmine\inventory\transaction\TransactionException;
|
use pocketmine\inventory\transaction\TransactionException;
|
||||||
use pocketmine\inventory\transaction\TransactionValidationException;
|
|
||||||
use pocketmine\item\VanillaItems;
|
use pocketmine\item\VanillaItems;
|
||||||
use pocketmine\item\WritableBook;
|
use pocketmine\item\WritableBook;
|
||||||
use pocketmine\item\WritableBookPage;
|
use pocketmine\item\WritableBookPage;
|
||||||
@ -65,6 +65,8 @@ use pocketmine\network\mcpe\protocol\EmotePacket;
|
|||||||
use pocketmine\network\mcpe\protocol\InteractPacket;
|
use pocketmine\network\mcpe\protocol\InteractPacket;
|
||||||
use pocketmine\network\mcpe\protocol\InventoryTransactionPacket;
|
use pocketmine\network\mcpe\protocol\InventoryTransactionPacket;
|
||||||
use pocketmine\network\mcpe\protocol\ItemFrameDropItemPacket;
|
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\LabTablePacket;
|
||||||
use pocketmine\network\mcpe\protocol\LecternUpdatePacket;
|
use pocketmine\network\mcpe\protocol\LecternUpdatePacket;
|
||||||
use pocketmine\network\mcpe\protocol\LevelSoundEventPacket;
|
use pocketmine\network\mcpe\protocol\LevelSoundEventPacket;
|
||||||
@ -96,7 +98,8 @@ use pocketmine\network\mcpe\protocol\types\inventory\MismatchTransactionData;
|
|||||||
use pocketmine\network\mcpe\protocol\types\inventory\NetworkInventoryAction;
|
use pocketmine\network\mcpe\protocol\types\inventory\NetworkInventoryAction;
|
||||||
use pocketmine\network\mcpe\protocol\types\inventory\NormalTransactionData;
|
use pocketmine\network\mcpe\protocol\types\inventory\NormalTransactionData;
|
||||||
use pocketmine\network\mcpe\protocol\types\inventory\ReleaseItemTransactionData;
|
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\UseItemOnEntityTransactionData;
|
||||||
use pocketmine\network\mcpe\protocol\types\inventory\UseItemTransactionData;
|
use pocketmine\network\mcpe\protocol\types\inventory\UseItemTransactionData;
|
||||||
use pocketmine\network\mcpe\protocol\types\PlayerAction;
|
use pocketmine\network\mcpe\protocol\types\PlayerAction;
|
||||||
@ -118,7 +121,6 @@ use function is_bool;
|
|||||||
use function is_infinite;
|
use function is_infinite;
|
||||||
use function is_nan;
|
use function is_nan;
|
||||||
use function json_decode;
|
use function json_decode;
|
||||||
use function json_encode;
|
|
||||||
use function max;
|
use function max;
|
||||||
use function mb_strlen;
|
use function mb_strlen;
|
||||||
use function microtime;
|
use function microtime;
|
||||||
@ -272,13 +274,22 @@ class InGamePacketHandler extends PacketHandler{
|
|||||||
if(count($useItemTransaction->getTransactionData()->getActions()) > 100){
|
if(count($useItemTransaction->getTransactionData()->getActions()) > 100){
|
||||||
throw new PacketHandlingException("Too many actions in item use transaction");
|
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())){
|
if(!$this->handleUseItemTransaction($useItemTransaction->getTransactionData())){
|
||||||
$packetHandled = false;
|
$packetHandled = false;
|
||||||
$this->session->getLogger()->debug("Unhandled transaction in PlayerAuthInputPacket (type " . $useItemTransaction->getTransactionData()->getActionType() . ")");
|
$this->session->getLogger()->debug("Unhandled transaction in PlayerAuthInputPacket (type " . $useItemTransaction->getTransactionData()->getActionType() . ")");
|
||||||
}else{
|
}else{
|
||||||
$this->inventoryManager->syncMismatchedPredictedSlotChanges();
|
$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;
|
return $packetHandled;
|
||||||
@ -312,14 +323,15 @@ class InGamePacketHandler extends PacketHandler{
|
|||||||
public function handleInventoryTransaction(InventoryTransactionPacket $packet) : bool{
|
public function handleInventoryTransaction(InventoryTransactionPacket $packet) : bool{
|
||||||
$result = true;
|
$result = true;
|
||||||
|
|
||||||
if(count($packet->trData->getActions()) > 100){
|
if(count($packet->trData->getActions()) > 50){
|
||||||
throw new PacketHandlingException("Too many actions in inventory transaction");
|
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){
|
if($packet->trData instanceof NormalTransactionData){
|
||||||
$result = $this->handleNormalTransaction($packet->trData);
|
$result = $this->handleNormalTransaction($packet->trData, $packet->requestId);
|
||||||
}elseif($packet->trData instanceof MismatchTransactionData){
|
}elseif($packet->trData instanceof MismatchTransactionData){
|
||||||
$this->session->getLogger()->debug("Mismatch transaction received");
|
$this->session->getLogger()->debug("Mismatch transaction received");
|
||||||
$this->inventoryManager->syncAll();
|
$this->inventoryManager->syncAll();
|
||||||
@ -335,93 +347,76 @@ class InGamePacketHandler extends PacketHandler{
|
|||||||
if($this->craftingTransaction === null){ //don't sync if we're waiting to complete a crafting transaction
|
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;
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function handleNormalTransaction(NormalTransactionData $data) : bool{
|
private function executeInventoryTransaction(InventoryTransaction $transaction, int $requestId) : bool{
|
||||||
/** @var InventoryAction[] $actions */
|
|
||||||
$actions = [];
|
|
||||||
|
|
||||||
$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;
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
$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){
|
$this->inventoryManager->setCurrentItemStackRequestId($requestId);
|
||||||
//TODO: 1.13+ often sends transactions with nothing but useless crap in them, no need for the debug noise
|
$this->inventoryManager->addTransactionPredictedSlotChanges($transaction);
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->player->setUsingItem(false);
|
|
||||||
$transaction = new InventoryTransaction($this->player, $actions);
|
|
||||||
try{
|
try{
|
||||||
$transaction->execute();
|
$transaction->execute();
|
||||||
}catch(TransactionException $e){
|
}catch(TransactionException $e){
|
||||||
$logger = $this->session->getLogger();
|
$logger = $this->session->getLogger();
|
||||||
$logger->debug("Failed to execute inventory transaction: " . $e->getMessage());
|
$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;
|
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();
|
||||||
|
$heldItem = $inventory->getItemInHand();
|
||||||
|
|
||||||
|
try{
|
||||||
|
$droppedItem = TypeConverter::getInstance()->netItemStackToCore($networkInventoryAction->newItem->getItemStack());
|
||||||
|
}catch(TypeConversionException $e){
|
||||||
|
throw PacketHandlingException::wrap($e);
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO: if we can avoid decoding incoming item NBT, it will be faster to compare network ItemStacks
|
||||||
|
//rather than converting to internal itemstacks and using canStackWith() here.
|
||||||
|
if(!$heldItem->canStackWith($droppedItem) || $heldItem->getCount() < $droppedItem->getCount()){
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
//purposely overwritten here - this allows any immutable internal references to be shared
|
||||||
|
$droppedItem = $heldItem->pop($droppedItem->getCount());
|
||||||
|
|
||||||
|
$builder = new TransactionBuilder();
|
||||||
|
$builder->getInventory($inventory)->setItem($inventory->getHeldItemIndex(), $heldItem);
|
||||||
|
$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{
|
private function handleUseItemTransaction(UseItemTransactionData $data) : bool{
|
||||||
$this->player->selectHotbarSlot($data->getHotbarSlot());
|
$this->player->selectHotbarSlot($data->getHotbarSlot());
|
||||||
|
|
||||||
@ -533,6 +528,26 @@ class InGamePacketHandler extends PacketHandler{
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function handleSingleItemStackRequest(ItemStackRequest $request) : ItemStackResponse{
|
||||||
|
$executor = new ItemStackRequestExecutor($this->player, $this->inventoryManager, $request);
|
||||||
|
$transaction = $executor->generateInventoryTransaction();
|
||||||
|
$result = $this->executeInventoryTransaction($transaction, $request->getRequestId());
|
||||||
|
$this->session->getLogger()->debug("Item stack request " . $request->getRequestId() . " result: " . ($result ? "success" : "failure"));
|
||||||
|
|
||||||
|
return $executor->buildItemStackResponse($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handleItemStackRequest(ItemStackRequestPacket $packet) : bool{
|
||||||
|
$responses = [];
|
||||||
|
foreach($packet->getRequests() as $request){
|
||||||
|
$responses[] = $this->handleSingleItemStackRequest($request);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->session->sendDataPacket(ItemStackResponsePacket::create($responses));
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
public function handleMobEquipment(MobEquipmentPacket $packet) : bool{
|
public function handleMobEquipment(MobEquipmentPacket $packet) : bool{
|
||||||
if($packet->windowId === ContainerIds::OFFHAND){
|
if($packet->windowId === ContainerIds::OFFHAND){
|
||||||
return true; //this happens when we put an item into the 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")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
313
src/network/mcpe/handler/ItemStackRequestExecutor.php
Normal file
313
src/network/mcpe/handler/ItemStackRequestExecutor.php
Normal file
@ -0,0 +1,313 @@
|
|||||||
|
<?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\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\network\PacketHandlingException;
|
||||||
|
use pocketmine\player\Player;
|
||||||
|
use pocketmine\utils\AssumptionFailedError;
|
||||||
|
use function array_key_first;
|
||||||
|
use function count;
|
||||||
|
use function get_class;
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @phpstan-return array{TransactionBuilderInventory, int}
|
||||||
|
*/
|
||||||
|
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 PacketHandlingException("Stack request action cannot target an inventory that is not open");
|
||||||
|
}
|
||||||
|
[$inventory, $slot] = $windowAndSlot;
|
||||||
|
if(!$inventory->slotExists($slot)){
|
||||||
|
throw new PacketHandlingException("Stack request action cannot target an inventory slot that does not exist");
|
||||||
|
}
|
||||||
|
|
||||||
|
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{
|
||||||
|
$removed = $this->removeItemFromSlot($source, $count);
|
||||||
|
$this->addItemToSlot($destination, $removed, $count);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deducts items from an inventory slot, returning a stack containing the removed items.
|
||||||
|
*/
|
||||||
|
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 PacketHandlingException("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 PacketHandlingException("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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 PacketHandlingException("Not all of the previous created item was taken");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$this->nextCreatedItem = $item;
|
||||||
|
$this->createdItemFromCreativeInventory = $creative;
|
||||||
|
$this->createdItemsTakenCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function beginCrafting(int $recipeId, int $repetitions) : void{
|
||||||
|
if($this->specialTransaction !== null){
|
||||||
|
throw new PacketHandlingException("Cannot perform more than 1 special action per request");
|
||||||
|
}
|
||||||
|
if($repetitions < 1){ //TODO: upper bound?
|
||||||
|
throw new PacketHandlingException("Cannot craft a recipe less than 1 time");
|
||||||
|
}
|
||||||
|
$craftingManager = $this->player->getServer()->getCraftingManager();
|
||||||
|
$recipe = $craftingManager->getCraftingRecipeIndex()[$recipeId] ?? null;
|
||||||
|
if($recipe === null){
|
||||||
|
throw new PacketHandlingException("Unknown crafting recipe ID $recipeId");
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->specialTransaction = new CraftingTransaction($this->player, $craftingManager, [], $recipe, $repetitions);
|
||||||
|
|
||||||
|
$currentWindow = $this->player->getCurrentWindow();
|
||||||
|
if($currentWindow !== null && !($currentWindow instanceof CraftingGrid)){
|
||||||
|
throw new PacketHandlingException("Cannot complete crafting when the 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 PacketHandlingException("No created item is waiting to be taken");
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!$this->createdItemFromCreativeInventory){
|
||||||
|
$availableCount = $createdItem->getCount() - $this->createdItemsTakenCount;
|
||||||
|
if($count > $availableCount){
|
||||||
|
throw new PacketHandlingException("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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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){
|
||||||
|
//TODO: the item may have been unregistered after the client was sent the creative contents, leaving a
|
||||||
|
//gap in the creative item list. This probably shouldn't be a violation, but I'm not sure how else to
|
||||||
|
//handle it right now.
|
||||||
|
throw new PacketHandlingException("Tried to create nonexisting creative item " . $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){
|
||||||
|
if(!$this->specialTransaction instanceof CraftingTransaction){
|
||||||
|
throw new PacketHandlingException("Cannot consume crafting input when no crafting transaction is in progress");
|
||||||
|
}
|
||||||
|
$this->removeItemFromSlot($action->getSource(), $action->getCount()); //output discarded - we allow CraftingTransaction to verify the balance
|
||||||
|
|
||||||
|
}elseif($action instanceof CraftingCreateSpecificResultStackRequestAction){
|
||||||
|
if(!$this->specialTransaction instanceof CraftingTransaction){
|
||||||
|
throw new AssumptionFailedError("Cannot mark crafting result index when no crafting transaction is in progress");
|
||||||
|
}
|
||||||
|
|
||||||
|
$nextResultItem = $this->craftingResults[$action->getResultIndex()] ?? null;
|
||||||
|
if($nextResultItem === null){
|
||||||
|
throw new PacketHandlingException("No such crafting result index " . $action->getResultIndex());
|
||||||
|
}
|
||||||
|
$this->setNextCreatedItem($nextResultItem);
|
||||||
|
}elseif($action instanceof DeprecatedCraftingResultsStackRequestAction){
|
||||||
|
//no obvious use
|
||||||
|
}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);
|
||||||
|
}
|
||||||
|
$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(bool $success) : ItemStackResponse{
|
||||||
|
$builder = new ItemStackResponseBuilder($this->request->getRequestId(), $this->inventoryManager);
|
||||||
|
foreach($this->requestSlotInfos as $requestInfo){
|
||||||
|
$builder->addSlot($requestInfo->getContainerId(), $requestInfo->getSlotId());
|
||||||
|
}
|
||||||
|
|
||||||
|
return $builder->build($success);
|
||||||
|
}
|
||||||
|
}
|
107
src/network/mcpe/handler/ItemStackResponseBuilder.php
Normal file
107
src/network/mcpe/handler/ItemStackResponseBuilder.php
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
<?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\network\PacketHandlingException;
|
||||||
|
|
||||||
|
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){
|
||||||
|
throw new PacketHandlingException("Stack request action cannot target an inventory that is not open");
|
||||||
|
}
|
||||||
|
[$inventory, $slot] = $windowAndSlot;
|
||||||
|
if(!$inventory->slotExists($slot)){
|
||||||
|
throw new PacketHandlingException("Stack request action cannot target an inventory slot that does not exist");
|
||||||
|
}
|
||||||
|
|
||||||
|
return [$inventory, $slot];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function build(bool $success) : ItemStackResponse{
|
||||||
|
$responseInfosByContainer = [];
|
||||||
|
foreach($this->changedSlots as $containerInterfaceId => $slotIds){
|
||||||
|
if($containerInterfaceId === ContainerUIIds::CREATED_OUTPUT){
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
foreach($slotIds as $slotId){
|
||||||
|
[$inventory, $slot] = $this->getInventoryAndSlot($containerInterfaceId, $slotId);
|
||||||
|
|
||||||
|
$itemStackInfo = $this->inventoryManager->getItemStackInfo($inventory, $slot);
|
||||||
|
if($itemStackInfo === null){
|
||||||
|
//TODO: what if a plugin closes the inventory while the transaction is ongoing?
|
||||||
|
throw new \LogicException("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)
|
||||||
|
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($success ? ItemStackResponse::RESULT_OK : ItemStackResponse::RESULT_ERROR, $this->requestId, $responseContainerInfos);
|
||||||
|
}
|
||||||
|
}
|
@ -98,7 +98,7 @@ class PreSpawnPacketHandler extends PacketHandler{
|
|||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
"",
|
"",
|
||||||
false,
|
true,
|
||||||
sprintf("%s %s", VersionInfo::NAME, VersionInfo::VERSION()->getFullVersion(true)),
|
sprintf("%s %s", VersionInfo::NAME, VersionInfo::VERSION()->getFullVersion(true)),
|
||||||
Uuid::fromString(Uuid::NIL),
|
Uuid::fromString(Uuid::NIL),
|
||||||
false,
|
false,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user