From 5afbb9d8072db2984899faf832d5c4e54ea1c7cd Mon Sep 17 00:00:00 2001 From: "Dylan K. Taylor" Date: Tue, 15 Aug 2023 19:10:03 +0100 Subject: [PATCH 1/7] Allow enchanted books to be enchanted if an enchanted book is obtained via /give without enchantments, it should be able to receive enchantments in an enchanting table, exactly the same as regular books. --- src/item/VanillaItems.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/item/VanillaItems.php b/src/item/VanillaItems.php index 07ba398d5..a5d50c9db 100644 --- a/src/item/VanillaItems.php +++ b/src/item/VanillaItems.php @@ -431,7 +431,7 @@ final class VanillaItems{ self::register("echo_shard", new Item(new IID(Ids::ECHO_SHARD), "Echo Shard")); self::register("egg", new Egg(new IID(Ids::EGG), "Egg")); self::register("emerald", new Item(new IID(Ids::EMERALD), "Emerald")); - self::register("enchanted_book", new EnchantedBook(new IID(Ids::ENCHANTED_BOOK), "Enchanted Book")); + self::register("enchanted_book", new EnchantedBook(new IID(Ids::ENCHANTED_BOOK), "Enchanted Book", [EnchantmentTags::ALL])); self::register("enchanted_golden_apple", new GoldenAppleEnchanted(new IID(Ids::ENCHANTED_GOLDEN_APPLE), "Enchanted Golden Apple")); self::register("ender_pearl", new EnderPearl(new IID(Ids::ENDER_PEARL), "Ender Pearl")); self::register("experience_bottle", new ExperienceBottle(new IID(Ids::EXPERIENCE_BOTTLE), "Bottle o' Enchanting")); From f516c3c50203555ce34b2049bf43fa78e0793f5c Mon Sep 17 00:00:00 2001 From: "Dylan K. Taylor" Date: Tue, 15 Aug 2023 19:10:48 +0100 Subject: [PATCH 2/7] EnchantCommand: ensure that books are turned into enchanted book items --- src/command/defaults/EnchantCommand.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/command/defaults/EnchantCommand.php b/src/command/defaults/EnchantCommand.php index 583bd59ec..189931b99 100644 --- a/src/command/defaults/EnchantCommand.php +++ b/src/command/defaults/EnchantCommand.php @@ -25,6 +25,7 @@ namespace pocketmine\command\defaults; use pocketmine\command\CommandSender; use pocketmine\command\utils\InvalidCommandSyntaxException; +use pocketmine\item\enchantment\EnchantmentHelper; use pocketmine\item\enchantment\EnchantmentInstance; use pocketmine\item\enchantment\StringToEnchantmentParser; use pocketmine\lang\KnownTranslationFactory; @@ -76,8 +77,9 @@ class EnchantCommand extends VanillaCommand{ } } - $item->addEnchantment(new EnchantmentInstance($enchantment, $level)); - $player->getInventory()->setItemInHand($item); + //this is necessary to deal with enchanted books, which are a different item type than regular books + $enchantedItem = EnchantmentHelper::enchantItem($item, [new EnchantmentInstance($enchantment, $level)]); + $player->getInventory()->setItemInHand($enchantedItem); self::broadcastCommandMessage($sender, KnownTranslationFactory::commands_enchant_success($player->getName())); return true; From b65b7a7f7497bc8e284eaedacbce61bd59c9ff88 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 16 Aug 2023 09:38:16 +0100 Subject: [PATCH 3/7] Bump tests/plugins/DevTools from `83f0db3` to `411fd5b` (#5998) Bumps [tests/plugins/DevTools](https://github.com/pmmp/DevTools) from `83f0db3` to `411fd5b`. - [Release notes](https://github.com/pmmp/DevTools/releases) - [Commits](https://github.com/pmmp/DevTools/compare/83f0db3f9e0adbf424e32ed81f7730e97b037be9...411fd5bdc002edd82cf1ea658d170bb814d59483) --- updated-dependencies: - dependency-name: tests/plugins/DevTools dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- tests/plugins/DevTools | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/plugins/DevTools b/tests/plugins/DevTools index 83f0db3f9..411fd5bdc 160000 --- a/tests/plugins/DevTools +++ b/tests/plugins/DevTools @@ -1 +1 @@ -Subproject commit 83f0db3f9e0adbf424e32ed81f7730e97b037be9 +Subproject commit 411fd5bdc002edd82cf1ea658d170bb814d59483 From e323c5dd76fa022af1c8be6d7ad82d4b9ccdd34a Mon Sep 17 00:00:00 2001 From: Dylan T Date: Wed, 16 Aug 2023 13:00:23 +0100 Subject: [PATCH 4/7] Implement pressure plate activation logic and events (#5991) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit closes #5936 This implements all of the basic activation logic for pressure plates. It also introduces a PressurePlateUpdateEvent, which is called in pulses when entities are standing on top of the plate and when it deactivates. Deactivation can be detected by checking if the list of activating entities is empty. --------- Co-authored-by: Javier León <58715544+JavierLeon9966@users.noreply.github.com> --- src/block/PressurePlate.php | 105 +++++++++++++++++- src/block/SimplePressurePlate.php | 16 +++ src/block/StonePressurePlate.php | 7 ++ src/block/VanillaBlocks.php | 20 +++- src/block/WeightedPressurePlate.php | 35 +++++- src/block/WeightedPressurePlateHeavy.php | 3 + src/block/WeightedPressurePlateLight.php | 3 + src/block/WoodenPressurePlate.php | 12 ++ src/event/block/PressurePlateUpdateEvent.php | 53 +++++++++ .../sound/PressurePlateActivateSound.php | 46 ++++++++ .../sound/PressurePlateDeactivateSound.php | 46 ++++++++ 11 files changed, 340 insertions(+), 6 deletions(-) create mode 100644 src/event/block/PressurePlateUpdateEvent.php create mode 100644 src/world/sound/PressurePlateActivateSound.php create mode 100644 src/world/sound/PressurePlateDeactivateSound.php diff --git a/src/block/PressurePlate.php b/src/block/PressurePlate.php index 4df0bf927..d67433a75 100644 --- a/src/block/PressurePlate.php +++ b/src/block/PressurePlate.php @@ -24,14 +24,33 @@ declare(strict_types=1); namespace pocketmine\block; use pocketmine\block\utils\SupportType; +use pocketmine\entity\Entity; +use pocketmine\event\block\PressurePlateUpdateEvent; use pocketmine\item\Item; +use pocketmine\math\Axis; +use pocketmine\math\AxisAlignedBB; use pocketmine\math\Facing; use pocketmine\math\Vector3; use pocketmine\player\Player; use pocketmine\world\BlockTransaction; +use pocketmine\world\sound\PressurePlateActivateSound; +use pocketmine\world\sound\PressurePlateDeactivateSound; +use function count; abstract class PressurePlate extends Transparent{ + private readonly int $deactivationDelayTicks; + + public function __construct( + BlockIdentifier $idInfo, + string $name, + BlockTypeInfo $typeInfo, + int $deactivationDelayTicks = 20 //TODO: make this mandatory in PM6 + ){ + parent::__construct($idInfo, $name, $typeInfo); + $this->deactivationDelayTicks = $deactivationDelayTicks; + } + public function isSolid() : bool{ return false; } @@ -61,5 +80,89 @@ abstract class PressurePlate extends Transparent{ } } - //TODO + public function hasEntityCollision() : bool{ + return true; + } + + public function onEntityInside(Entity $entity) : bool{ + if(!$this->hasOutputSignal()){ + $this->position->getWorld()->scheduleDelayedBlockUpdate($this->position, 0); + } + return true; + } + + /** + * Returns the AABB that entities must intersect to activate the pressure plate. + * Note that this is not the same as the collision box (pressure plate doesn't have one), nor the visual bounding + * box. The activation area has a height of 0.25 blocks. + */ + protected function getActivationBox() : AxisAlignedBB{ + return AxisAlignedBB::one() + ->squash(Axis::X, 1 / 8) + ->squash(Axis::Z, 1 / 8) + ->trim(Facing::UP, 3 / 4) + ->offset($this->position->x, $this->position->y, $this->position->z); + } + + /** + * TODO: make this abstract in PM6 + */ + protected function hasOutputSignal() : bool{ + return false; + } + + /** + * TODO: make this abstract in PM6 + * + * @param Entity[] $entities + * + * @return mixed[] + * @phpstan-return array{Block, ?bool} + */ + protected function calculatePlateState(array $entities) : array{ + return [$this, null]; + } + + /** + * Filters entities which don't affect the pressure plate state from the given list. + * + * @param Entity[] $entities + * @return Entity[] + */ + protected function filterIrrelevantEntities(array $entities) : array{ + return $entities; + } + + public function onScheduledUpdate() : void{ + $world = $this->position->getWorld(); + + $intersectionAABB = $this->getActivationBox(); + $activatingEntities = $this->filterIrrelevantEntities($world->getNearbyEntities($intersectionAABB)); + + //if an irrelevant entity is inside the full cube space of the pressure plate but not activating the plate, + //it will cause scheduled updates on the plate every tick. We don't want to fire events in this case if the + //plate is already deactivated. + if(count($activatingEntities) > 0 || $this->hasOutputSignal()){ + [$newState, $pressedChange] = $this->calculatePlateState($activatingEntities); + + //always call this, in case there are new entities on the plate + if(PressurePlateUpdateEvent::hasHandlers()){ + $ev = new PressurePlateUpdateEvent($this, $newState, $activatingEntities); + $ev->call(); + $newState = $ev->isCancelled() ? null : $ev->getNewState(); + } + if($newState !== null){ + $world->setBlock($this->position, $newState); + if($pressedChange !== null){ + $world->addSound($this->position, $pressedChange ? + new PressurePlateActivateSound($this) : + new PressurePlateDeactivateSound($this) + ); + } + } + if($pressedChange ?? $this->hasOutputSignal()){ + $world->scheduleDelayedBlockUpdate($this->position, $this->deactivationDelayTicks); + } + } + } } diff --git a/src/block/SimplePressurePlate.php b/src/block/SimplePressurePlate.php index e4278410d..3429b9b5d 100644 --- a/src/block/SimplePressurePlate.php +++ b/src/block/SimplePressurePlate.php @@ -24,6 +24,7 @@ declare(strict_types=1); namespace pocketmine\block; use pocketmine\data\runtime\RuntimeDataDescriber; +use function count; abstract class SimplePressurePlate extends PressurePlate{ protected bool $pressed = false; @@ -39,4 +40,19 @@ abstract class SimplePressurePlate extends PressurePlate{ $this->pressed = $pressed; return $this; } + + protected function hasOutputSignal() : bool{ + return $this->pressed; + } + + protected function calculatePlateState(array $entities) : array{ + $newPressed = count($entities) > 0; + if($newPressed === $this->pressed){ + return [$this, null]; + } + return [ + (clone $this)->setPressed($newPressed), + $newPressed + ]; + } } diff --git a/src/block/StonePressurePlate.php b/src/block/StonePressurePlate.php index 626e6d885..5ddc5a599 100644 --- a/src/block/StonePressurePlate.php +++ b/src/block/StonePressurePlate.php @@ -23,6 +23,13 @@ declare(strict_types=1); namespace pocketmine\block; +use pocketmine\entity\Entity; +use pocketmine\entity\Living; +use function array_filter; + class StonePressurePlate extends SimplePressurePlate{ + protected function filterIrrelevantEntities(array $entities) : array{ + return array_filter($entities, fn(Entity $e) => $e instanceof Living); //TODO: armor stands should activate stone plates too + } } diff --git a/src/block/VanillaBlocks.php b/src/block/VanillaBlocks.php index cb612031f..a41c3985b 100644 --- a/src/block/VanillaBlocks.php +++ b/src/block/VanillaBlocks.php @@ -1114,8 +1114,20 @@ final class VanillaBlocks{ self::register("lily_pad", new WaterLily(new BID(Ids::LILY_PAD), "Lily Pad", new Info(BreakInfo::instant()))); $weightedPressurePlateBreakInfo = new Info(BreakInfo::pickaxe(0.5, ToolTier::WOOD())); - self::register("weighted_pressure_plate_heavy", new WeightedPressurePlateHeavy(new BID(Ids::WEIGHTED_PRESSURE_PLATE_HEAVY), "Weighted Pressure Plate Heavy", $weightedPressurePlateBreakInfo)); - self::register("weighted_pressure_plate_light", new WeightedPressurePlateLight(new BID(Ids::WEIGHTED_PRESSURE_PLATE_LIGHT), "Weighted Pressure Plate Light", $weightedPressurePlateBreakInfo)); + self::register("weighted_pressure_plate_heavy", new WeightedPressurePlateHeavy( + new BID(Ids::WEIGHTED_PRESSURE_PLATE_HEAVY), + "Weighted Pressure Plate Heavy", + $weightedPressurePlateBreakInfo, + deactivationDelayTicks: 10, + signalStrengthFactor: 0.1 + )); + self::register("weighted_pressure_plate_light", new WeightedPressurePlateLight( + new BID(Ids::WEIGHTED_PRESSURE_PLATE_LIGHT), + "Weighted Pressure Plate Light", + $weightedPressurePlateBreakInfo, + deactivationDelayTicks: 10, + signalStrengthFactor: 1.0 + )); self::register("wheat", new Wheat(new BID(Ids::WHEAT), "Wheat Block", new Info(BreakInfo::instant()))); $leavesBreakInfo = new Info(new class(0.2, ToolType::HOE) extends BreakInfo{ @@ -1266,7 +1278,7 @@ final class VanillaBlocks{ self::register($idName("door"), new WoodenDoor(WoodLikeBlockIdHelper::getDoorIdentifier($woodType), $name . " Door", $woodenDoorBreakInfo, $woodType)); self::register($idName("button"), new WoodenButton(WoodLikeBlockIdHelper::getButtonIdentifier($woodType), $name . " Button", $woodenButtonBreakInfo, $woodType)); - self::register($idName("pressure_plate"), new WoodenPressurePlate(WoodLikeBlockIdHelper::getPressurePlateIdentifier($woodType), $name . " Pressure Plate", $woodenPressurePlateBreakInfo, $woodType)); + self::register($idName("pressure_plate"), new WoodenPressurePlate(WoodLikeBlockIdHelper::getPressurePlateIdentifier($woodType), $name . " Pressure Plate", $woodenPressurePlateBreakInfo, $woodType, 20)); self::register($idName("trapdoor"), new WoodenTrapdoor(WoodLikeBlockIdHelper::getTrapdoorIdentifier($woodType), $name . " Trapdoor", $woodenDoorBreakInfo, $woodType)); [$floorSignId, $wallSignId, $signAsItem] = WoodLikeBlockIdHelper::getSignInfo($woodType); @@ -1491,7 +1503,7 @@ final class VanillaBlocks{ $prefix = fn(string $thing) => "Polished Blackstone" . ($thing !== "" ? " $thing" : ""); self::register("polished_blackstone", new Opaque(new BID(Ids::POLISHED_BLACKSTONE), $prefix(""), $blackstoneBreakInfo)); self::register("polished_blackstone_button", new StoneButton(new BID(Ids::POLISHED_BLACKSTONE_BUTTON), $prefix("Button"), new Info(BreakInfo::pickaxe(0.5)))); - self::register("polished_blackstone_pressure_plate", new StonePressurePlate(new BID(Ids::POLISHED_BLACKSTONE_PRESSURE_PLATE), $prefix("Pressure Plate"), new Info(BreakInfo::pickaxe(0.5, ToolTier::WOOD())))); + self::register("polished_blackstone_pressure_plate", new StonePressurePlate(new BID(Ids::POLISHED_BLACKSTONE_PRESSURE_PLATE), $prefix("Pressure Plate"), new Info(BreakInfo::pickaxe(0.5, ToolTier::WOOD())), 20)); self::register("polished_blackstone_slab", new Slab(new BID(Ids::POLISHED_BLACKSTONE_SLAB), $prefix(""), $slabBreakInfo)); self::register("polished_blackstone_stairs", new Stair(new BID(Ids::POLISHED_BLACKSTONE_STAIRS), $prefix("Stairs"), $blackstoneBreakInfo)); self::register("polished_blackstone_wall", new Wall(new BID(Ids::POLISHED_BLACKSTONE_WALL), $prefix("Wall"), $blackstoneBreakInfo)); diff --git a/src/block/WeightedPressurePlate.php b/src/block/WeightedPressurePlate.php index bdfae5082..726b31f6b 100644 --- a/src/block/WeightedPressurePlate.php +++ b/src/block/WeightedPressurePlate.php @@ -24,7 +24,40 @@ declare(strict_types=1); namespace pocketmine\block; use pocketmine\block\utils\AnalogRedstoneSignalEmitterTrait; +use function ceil; +use function count; +use function max; +use function min; -abstract class WeightedPressurePlate extends PressurePlate{ +class WeightedPressurePlate extends PressurePlate{ use AnalogRedstoneSignalEmitterTrait; + + private readonly float $signalStrengthFactor; + + /** + * @param float $signalStrengthFactor Number of entities on the plate is divided by this value to get signal strength + */ + public function __construct(BlockIdentifier $idInfo, string $name, BlockTypeInfo $typeInfo, int $deactivationDelayTicks, float $signalStrengthFactor = 1.0){ + parent::__construct($idInfo, $name, $typeInfo, $deactivationDelayTicks); + $this->signalStrengthFactor = $signalStrengthFactor; + } + + protected function hasOutputSignal() : bool{ + return $this->signalStrength > 0; + } + + protected function calculatePlateState(array $entities) : array{ + $newSignalStrength = min(15, max(0, + (int) ceil(count($entities) * $this->signalStrengthFactor) + )); + if($newSignalStrength === $this->signalStrength){ + return [$this, null]; + } + $wasActive = $this->signalStrength !== 0; + $isActive = $newSignalStrength !== 0; + return [ + (clone $this)->setOutputSignalStrength($newSignalStrength), + $wasActive !== $isActive ? $isActive : null + ]; + } } diff --git a/src/block/WeightedPressurePlateHeavy.php b/src/block/WeightedPressurePlateHeavy.php index 390297436..9a8d1c31b 100644 --- a/src/block/WeightedPressurePlateHeavy.php +++ b/src/block/WeightedPressurePlateHeavy.php @@ -23,6 +23,9 @@ declare(strict_types=1); namespace pocketmine\block; +/** + * @deprecated + */ class WeightedPressurePlateHeavy extends WeightedPressurePlate{ } diff --git a/src/block/WeightedPressurePlateLight.php b/src/block/WeightedPressurePlateLight.php index 458c07e1a..85c13d438 100644 --- a/src/block/WeightedPressurePlateLight.php +++ b/src/block/WeightedPressurePlateLight.php @@ -23,6 +23,9 @@ declare(strict_types=1); namespace pocketmine\block; +/** + * @deprecated + */ class WeightedPressurePlateLight extends WeightedPressurePlate{ } diff --git a/src/block/WoodenPressurePlate.php b/src/block/WoodenPressurePlate.php index baaf44c2c..a629c2f1c 100644 --- a/src/block/WoodenPressurePlate.php +++ b/src/block/WoodenPressurePlate.php @@ -23,11 +23,23 @@ declare(strict_types=1); namespace pocketmine\block; +use pocketmine\block\utils\WoodType; use pocketmine\block\utils\WoodTypeTrait; class WoodenPressurePlate extends SimplePressurePlate{ use WoodTypeTrait; + public function __construct( + BlockIdentifier $idInfo, + string $name, + BlockTypeInfo $typeInfo, + WoodType $woodType, + int $deactivationDelayTicks = 20 //TODO: make this mandatory in PM6 + ){ + $this->woodType = $woodType; + parent::__construct($idInfo, $name, $typeInfo, $deactivationDelayTicks); + } + public function getFuelTime() : int{ return 300; } diff --git a/src/event/block/PressurePlateUpdateEvent.php b/src/event/block/PressurePlateUpdateEvent.php new file mode 100644 index 000000000..485a3a6be --- /dev/null +++ b/src/event/block/PressurePlateUpdateEvent.php @@ -0,0 +1,53 @@ +activatingEntities; } +} diff --git a/src/world/sound/PressurePlateActivateSound.php b/src/world/sound/PressurePlateActivateSound.php new file mode 100644 index 000000000..fac24e285 --- /dev/null +++ b/src/world/sound/PressurePlateActivateSound.php @@ -0,0 +1,46 @@ +getBlockTranslator()->internalIdToNetworkId($this->block->getStateId()) + )]; + } +} diff --git a/src/world/sound/PressurePlateDeactivateSound.php b/src/world/sound/PressurePlateDeactivateSound.php new file mode 100644 index 000000000..895bb8b8a --- /dev/null +++ b/src/world/sound/PressurePlateDeactivateSound.php @@ -0,0 +1,46 @@ +getBlockTranslator()->internalIdToNetworkId($this->block->getStateId()) + )]; + } +} From beaca8bb6df5fe0d88b5ba77d5b5279b0f7222be Mon Sep 17 00:00:00 2001 From: "Dylan K. Taylor" Date: Wed, 16 Aug 2023 14:51:47 +0100 Subject: [PATCH 5/7] EnchantTransaction: fixed XP level costs when minimum level is less than the XP cost this can happen and happens in vanilla too. In these cases, as much of the XP cost as possible is deducted. --- src/inventory/transaction/EnchantTransaction.php | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/inventory/transaction/EnchantTransaction.php b/src/inventory/transaction/EnchantTransaction.php index f3760e479..3d9fd7e06 100644 --- a/src/inventory/transaction/EnchantTransaction.php +++ b/src/inventory/transaction/EnchantTransaction.php @@ -56,6 +56,15 @@ class EnchantTransaction extends InventoryTransaction{ } } + /** + * The selected option might be available to a player who has enough XP levels to meet the option's minimum level, + * but not enough to pay the full cost (e.g. option costs 3 levels but requires only 1 to use). As much XP as + * possible is spent in these cases. + */ + private function getAdjustedXpCost() : int{ + return min($this->cost, $this->source->getXpManager()->getXpLevel()); + } + private function validateFiniteResources(int $lapisSpent) : void{ if($lapisSpent !== $this->cost){ throw new TransactionValidationException("Expected the amount of lapis lazuli spent to be $this->cost, but received $lapisSpent"); @@ -63,12 +72,13 @@ class EnchantTransaction extends InventoryTransaction{ $xpLevel = $this->source->getXpManager()->getXpLevel(); $requiredXpLevel = $this->option->getRequiredXpLevel(); + $actualCost = $this->getAdjustedXpCost(); if($xpLevel < $requiredXpLevel){ throw new TransactionValidationException("Player's XP level $xpLevel is less than the required XP level $requiredXpLevel"); } - if($xpLevel < $this->cost){ - throw new TransactionValidationException("Player's XP level $xpLevel is less than the XP level cost $this->cost"); + if($xpLevel < $actualCost){ + throw new TransactionValidationException("Player's XP level $xpLevel is less than the XP level cost $actualCost"); } } @@ -115,7 +125,7 @@ class EnchantTransaction extends InventoryTransaction{ parent::execute(); if($this->source->hasFiniteResources()){ - $this->source->getXpManager()->subtractXpLevels($this->cost); + $this->source->getXpManager()->subtractXpLevels($this->getAdjustedXpCost()); } $this->source->setEnchantmentSeed($this->source->generateEnchantmentSeed()); } From 9f09acc07933d037af9c1f1f683ca0f3cd6a6215 Mon Sep 17 00:00:00 2001 From: "Dylan K. Taylor" Date: Fri, 18 Aug 2023 12:27:27 +0100 Subject: [PATCH 6/7] Workaround for slot IDs not changing client side when old item == new item this is a really dumb bug and seems similar to the armor bug I fixed a while ago. fixes #5987 it's unlikely that #5727 will be solved by this, but one can hope... --- src/network/mcpe/InventoryManager.php | 73 ++++++++++++++++++--------- 1 file changed, 50 insertions(+), 23 deletions(-) diff --git a/src/network/mcpe/InventoryManager.php b/src/network/mcpe/InventoryManager.php index c6d83c65e..9982ad8d3 100644 --- a/src/network/mcpe/InventoryManager.php +++ b/src/network/mcpe/InventoryManager.php @@ -59,6 +59,7 @@ use pocketmine\network\PacketHandlingException; use pocketmine\player\Player; use pocketmine\utils\AssumptionFailedError; use pocketmine\utils\ObjectSet; +use function array_fill_keys; use function array_keys; use function array_search; use function count; @@ -429,6 +430,50 @@ class InventoryManager{ unset($inventoryEntry->predictions[$slot]); } + private function sendInventorySlotPackets(int $windowId, int $netSlot, ItemStackWrapper $itemStackWrapper) : void{ + /* + * TODO: HACK! + * As of 1.20.12, the client ignores change of itemstackID in some cases when the old item == the new item. + * Notably, this happens with armor, offhand and enchanting tables, but not with main inventory. + * While we could track the items previously sent to the client, that's a waste of memory and would + * cost performance. Instead, clear the slot(s) first, then send the new item(s). + * The network cost of doing this is fortunately minimal, as an air itemstack is only 1 byte. + */ + if($itemStackWrapper->getStackId() !== 0){ + $this->session->sendDataPacket(InventorySlotPacket::create( + $windowId, + $netSlot, + new ItemStackWrapper(0, ItemStack::null()) + )); + } + //now send the real contents + $this->session->sendDataPacket(InventorySlotPacket::create( + $windowId, + $netSlot, + $itemStackWrapper + )); + } + + /** + * @param ItemStackWrapper[] $itemStackWrappers + */ + private function sendInventoryContentPackets(int $windowId, array $itemStackWrappers) : void{ + /* + * TODO: HACK! + * As of 1.20.12, the client ignores change of itemstackID in some cases when the old item == the new item. + * Notably, this happens with armor, offhand and enchanting tables, but not with main inventory. + * While we could track the items previously sent to the client, that's a waste of memory and would + * cost performance. Instead, clear the slot(s) first, then send the new item(s). + * The network cost of doing this is fortunately minimal, as an air itemstack is only 1 byte. + */ + $this->session->sendDataPacket(InventoryContentPacket::create( + $windowId, + array_fill_keys(array_keys($itemStackWrappers), new ItemStackWrapper(0, ItemStack::null())) + )); + //now send the real contents + $this->session->sendDataPacket(InventoryContentPacket::create($windowId, $itemStackWrappers)); + } + public function syncSlot(Inventory $inventory, int $slot, ItemStack $itemStack) : void{ $entry = $this->inventories[spl_object_id($inventory)] ?? null; if($entry === null){ @@ -453,24 +498,9 @@ class InventoryManager{ //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])); + $this->sendInventoryContentPackets($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()))); - } - $this->session->sendDataPacket(InventorySlotPacket::create($windowId, $netSlot, $itemStackWrapper)); + $this->sendInventorySlotPackets($windowId, $netSlot, $itemStackWrapper); } unset($entry->predictions[$slot], $entry->pendingSyncs[$slot]); } @@ -497,20 +527,17 @@ class InventoryManager{ $info = $this->trackItemStack($entry, $slot, $itemStack, null); $contents[] = new ItemStackWrapper($info->getStackId(), $itemStack); } + $clearSlotWrapper = new ItemStackWrapper(0, ItemStack::null()); if($entry->complexSlotMap !== null){ foreach($contents as $slotId => $info){ $packetSlot = $entry->complexSlotMap->mapCoreToNet($slotId) ?? null; if($packetSlot === null){ continue; } - $this->session->sendDataPacket(InventorySlotPacket::create( - $windowId, - $packetSlot, - $info - )); + $this->sendInventorySlotPackets($windowId, $packetSlot, $info); } }else{ - $this->session->sendDataPacket(InventoryContentPacket::create($windowId, $contents)); + $this->sendInventoryContentPackets($windowId, $contents); } } } From b2414b4c29f54543fe2a0e1b3ecbf7bc580fdbab Mon Sep 17 00:00:00 2001 From: "Dylan K. Taylor" Date: Fri, 18 Aug 2023 12:33:07 +0100 Subject: [PATCH 7/7] EnchantTransaction: cleanup XP cost checking logic --- .../transaction/EnchantTransaction.php | 20 ++++++------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/src/inventory/transaction/EnchantTransaction.php b/src/inventory/transaction/EnchantTransaction.php index 3d9fd7e06..5ea8a997f 100644 --- a/src/inventory/transaction/EnchantTransaction.php +++ b/src/inventory/transaction/EnchantTransaction.php @@ -31,6 +31,7 @@ use pocketmine\item\ItemTypeIds; use pocketmine\player\Player; use pocketmine\utils\AssumptionFailedError; use function count; +use function min; class EnchantTransaction extends InventoryTransaction{ @@ -56,15 +57,6 @@ class EnchantTransaction extends InventoryTransaction{ } } - /** - * The selected option might be available to a player who has enough XP levels to meet the option's minimum level, - * but not enough to pay the full cost (e.g. option costs 3 levels but requires only 1 to use). As much XP as - * possible is spent in these cases. - */ - private function getAdjustedXpCost() : int{ - return min($this->cost, $this->source->getXpManager()->getXpLevel()); - } - private function validateFiniteResources(int $lapisSpent) : void{ if($lapisSpent !== $this->cost){ throw new TransactionValidationException("Expected the amount of lapis lazuli spent to be $this->cost, but received $lapisSpent"); @@ -72,14 +64,12 @@ class EnchantTransaction extends InventoryTransaction{ $xpLevel = $this->source->getXpManager()->getXpLevel(); $requiredXpLevel = $this->option->getRequiredXpLevel(); - $actualCost = $this->getAdjustedXpCost(); if($xpLevel < $requiredXpLevel){ throw new TransactionValidationException("Player's XP level $xpLevel is less than the required XP level $requiredXpLevel"); } - if($xpLevel < $actualCost){ - throw new TransactionValidationException("Player's XP level $xpLevel is less than the XP level cost $actualCost"); - } + //XP level cost is intentionally not checked here, as the required level may be lower than the cost, allowing + //the option to be used with less XP than the cost - in this case, as much XP as possible will be deducted. } public function validate() : void{ @@ -125,7 +115,9 @@ class EnchantTransaction extends InventoryTransaction{ parent::execute(); if($this->source->hasFiniteResources()){ - $this->source->getXpManager()->subtractXpLevels($this->getAdjustedXpCost()); + //If the required XP level is less than the XP cost, the option can be selected with less XP than the cost. + //In this case, as much XP as possible will be taken. + $this->source->getXpManager()->subtractXpLevels(min($this->cost, $this->source->getXpManager()->getXpLevel())); } $this->source->setEnchantmentSeed($this->source->generateEnchantmentSeed()); }