PlayerInteractEvent: added APIs to independently control reaction of item and block (#4683)

This allows, for example, banning the usage of spawn eggs, without preventing opening of doors, without the need for item ID whitelists.

It also allows customizing the behaviour of item and block interactions when sneaking - it's now possible to force spawn eggs to work when sneaking, or force containers to open.

Finally, this also allows preventing any interaction at all without preventing block placement (by setting both to false). Since cancelling the event will typically prevent placement too (which might not be desired).

Side note: Blocks are now always synced when right-clicking on a block.
This differs from the previous behaviour, where the blocks were only synced when the action "failed".
However, since this change introduces a situation where the action may succeed but have different results than the client expects, it's best to just always sync blocks in this situation.

Fixes #3267
This commit is contained in:
Dylan T. 2024-11-15 21:19:54 +00:00 committed by GitHub
parent 4b630cb726
commit ff695a5f97
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 48 additions and 12 deletions

View File

@ -42,6 +42,9 @@ class PlayerInteractEvent extends PlayerEvent implements Cancellable{
protected Vector3 $touchVector;
protected bool $useItem = true;
protected bool $useBlock = true;
public function __construct(
Player $player,
protected Item $item,
@ -73,4 +76,28 @@ class PlayerInteractEvent extends PlayerEvent implements Cancellable{
public function getFace() : int{
return $this->blockFace;
}
/**
* Returns whether the item may react to the interaction. If disabled, items such as spawn eggs will not activate.
* This does NOT prevent blocks from being placed - it makes the item behave as if the player is sneaking.
*/
public function useItem() : bool{ return $this->useItem; }
/**
* Sets whether the used item may react to the interaction. If false, items such as spawn eggs will not activate.
* This does NOT prevent blocks from being placed - it makes the item behave as if the player is sneaking.
*/
public function setUseItem(bool $useItem) : void{ $this->useItem = $useItem; }
/**
* Returns whether the block may react to the interaction. If false, doors, fence gates and trapdoors will not
* respond, containers will not open, etc.
*/
public function useBlock() : bool{ return $this->useBlock; }
/**
* Sets whether the block may react to the interaction. If false, doors, fence gates and trapdoors will not
* respond, containers will not open, etc.
*/
public function setUseBlock(bool $useBlock) : void{ $this->useBlock = $useBlock; }
}

View File

@ -493,15 +493,18 @@ class InGamePacketHandler extends PacketHandler{
$blockPos = $data->getBlockPosition();
$vBlockPos = new Vector3($blockPos->getX(), $blockPos->getY(), $blockPos->getZ());
if(!$this->player->interactBlock($vBlockPos, $data->getFace(), $clickPos)){
$this->onFailedBlockAction($vBlockPos, $data->getFace());
}
$this->player->interactBlock($vBlockPos, $data->getFace(), $clickPos);
//always sync this in case plugins caused a different result than the client expected
//we *could* try to enhance detection of plugin-altered behaviour, but this would require propagating
//more information up the stack. For now I think this is good enough.
//if only the client would tell us what blocks it thinks changed...
$this->syncBlocksNearby($vBlockPos, $data->getFace());
return true;
case UseItemTransactionData::ACTION_BREAK_BLOCK:
$blockPos = $data->getBlockPosition();
$vBlockPos = new Vector3($blockPos->getX(), $blockPos->getY(), $blockPos->getZ());
if(!$this->player->breakBlock($vBlockPos)){
$this->onFailedBlockAction($vBlockPos, null);
$this->syncBlocksNearby($vBlockPos, null);
}
return true;
case UseItemTransactionData::ACTION_CLICK_AIR:
@ -529,9 +532,9 @@ class InGamePacketHandler extends PacketHandler{
}
/**
* Internal function used to execute rollbacks when an action fails on a block.
* Syncs blocks nearby to ensure that the client and server agree on the world's blocks after a block interaction.
*/
private function onFailedBlockAction(Vector3 $blockPos, ?int $face) : void{
private function syncBlocksNearby(Vector3 $blockPos, ?int $face) : void{
if($blockPos->distanceSquared($this->player->getLocation()) < 10000){
$blocks = $blockPos->sidesArray();
if($face !== null){
@ -682,7 +685,7 @@ class InGamePacketHandler extends PacketHandler{
case PlayerAction::START_BREAK:
self::validateFacing($face);
if(!$this->player->attackBlock($pos, $face)){
$this->onFailedBlockAction($pos, $face);
$this->syncBlocksNearby($pos, $face);
}
break;
@ -998,7 +1001,7 @@ class InGamePacketHandler extends PacketHandler{
$lectern = $world->getBlockAt($pos->getX(), $pos->getY(), $pos->getZ());
if($lectern instanceof Lectern && $this->player->canInteract($lectern->getPosition(), 15)){
if(!$lectern->onPageTurn($packet->page)){
$this->onFailedBlockAction($lectern->getPosition(), null);
$this->syncBlocksNearby($lectern->getPosition(), null);
}
return true;
}

View File

@ -2173,19 +2173,25 @@ class World implements ChunkManager{
if($player !== null){
$ev = new PlayerInteractEvent($player, $item, $blockClicked, $clickVector, $face, PlayerInteractEvent::RIGHT_CLICK_BLOCK);
if($player->isSneaking()){
$ev->setUseItem(false);
$ev->setUseBlock($item->isNull()); //opening doors is still possible when sneaking if using an empty hand
}
if($player->isSpectator()){
$ev->cancel(); //set it to cancelled so plugins can bypass this
}
$ev->call();
if(!$ev->isCancelled()){
if((!$player->isSneaking() || $item->isNull()) && $blockClicked->onInteract($item, $face, $clickVector, $player, $returnedItems)){
if($ev->useBlock() && $blockClicked->onInteract($item, $face, $clickVector, $player, $returnedItems)){
return true;
}
$result = $item->onInteractBlock($player, $blockReplace, $blockClicked, $face, $clickVector, $returnedItems);
if($result !== ItemUseResult::NONE){
return $result === ItemUseResult::SUCCESS;
if($ev->useItem()){
$result = $item->onInteractBlock($player, $blockReplace, $blockClicked, $face, $clickVector, $returnedItems);
if($result !== ItemUseResult::NONE){
return $result === ItemUseResult::SUCCESS;
}
}
}else{
return false;