feat: add plugin events for entity interaction and block placement (#1572)

* feat: add cancellable PlayerInteractEntityEvent for plugin API

* feat: add cancellable PlayerInteractUnknownEntityEvent for plugin API

* feat: fire cancellable BlockPlaceEvent in block placement flow

* fix: add backticks to InteractAt in doc comments for clippy
This commit is contained in:
Richard Marshall
2026-02-14 23:53:28 +00:00
committed by GitHub
parent a61238a498
commit 74e1cd1f5b
5 changed files with 219 additions and 64 deletions

View File

@@ -20,9 +20,12 @@ use crate::error::PumpkinError;
use crate::log_at_level;
use crate::net::PlayerConfig;
use crate::net::java::JavaClient;
use crate::plugin::block::block_place::BlockPlaceEvent;
use crate::plugin::player::player_chat::PlayerChatEvent;
use crate::plugin::player::player_command_send::PlayerCommandSendEvent;
use crate::plugin::player::player_interact_entity_event::PlayerInteractEntityEvent;
use crate::plugin::player::player_interact_event::{InteractAction, PlayerInteractEvent};
use crate::plugin::player::player_interact_unknown_entity_event::PlayerInteractUnknownEntityEvent;
use crate::plugin::player::player_move::PlayerMoveEvent;
use crate::server::{Server, seasonal_events};
use crate::world::{World, chunker};
@@ -1195,7 +1198,7 @@ impl JavaClient {
pub async fn handle_interact(
&self,
player: &Player,
player: &Arc<Player>,
interact: SInteract,
server: &Arc<Server>,
) {
@@ -1215,73 +1218,93 @@ impl JavaClient {
return;
};
match action {
ActionType::Attack => {
let config = &server.advanced_config.pvp;
// TODO: do validation and stuff
if !config.enabled {
return;
}
// Resolve the target entity for the event
let world = player_entity.world.load_full();
let player_target = world.get_player_by_id(entity_id.0);
let target: Option<Arc<dyn EntityBase>> = player_target
.as_ref()
.map(|p| Arc::clone(p) as Arc<dyn EntityBase>)
.or_else(|| world.get_entity_by_id(entity_id.0));
// TODO: set as camera entity when spectator
if let Some(target) = target {
send_cancellable! {{
server;
PlayerInteractEntityEvent::new(
player,
Arc::clone(&target),
action.clone(),
interact.target_position,
sneaking,
);
let world = player_entity.world.load_full();
let player_victim = world.get_player_by_id(entity_id.0);
if entity_id.0 == player.entity_id() {
// This can't be triggered from a non-modded client.
self.kick(TextComponent::translate(
translation::MULTIPLAYER_DISCONNECT_INVALID_ENTITY_ATTACKED,
[],
))
.await;
return;
}
if let Some(player_victim) = player_victim {
if player_victim.living_entity.health.load() <= 0.0 {
// You can trigger this from a non-modded / innocent client,
// so we shouldn't kick the player.
return;
'after: {
match event.action {
ActionType::Attack => {
let config = &server.advanced_config.pvp;
if !config.enabled {
return;
}
if entity_id.0 == player.entity_id() {
self.kick(TextComponent::translate(
translation::MULTIPLAYER_DISCONNECT_INVALID_ENTITY_ATTACKED,
[],
))
.await;
return;
}
if let Some(player_victim) = &player_target {
if player_victim.living_entity.health.load() <= 0.0 {
return;
}
if config.protect_creative
&& player_victim.gamemode.load() == GameMode::Creative
{
world
.play_sound(
Sound::EntityPlayerAttackNodamage,
SoundCategory::Players,
&player_victim.position(),
)
.await;
return;
}
}
player.attack(event.target).await;
}
ActionType::Interact | ActionType::InteractAt => {
let held = player.inventory.held_item();
let mut stack = held.lock().await;
server
.item_registry
.use_on_entity(&mut stack, player, event.target)
.await;
}
}
if config.protect_creative
&& player_victim.gamemode.load() == GameMode::Creative
{
world
.play_sound(
Sound::EntityPlayerAttackNodamage,
SoundCategory::Players,
&player_victim.position(),
)
.await;
return;
}
player.attack(player_victim).await;
} else if let Some(entity_victim) = world.get_entity_by_id(entity_id.0) {
player.attack(entity_victim).await;
} else {
error!(
"Player id {} interacted with entity id {}, which was not found.",
player.entity_id(),
entity_id.0
);
self.kick(TextComponent::translate(
translation::MULTIPLAYER_DISCONNECT_INVALID_ENTITY_ATTACKED,
[],
))
.await;
}
}
ActionType::Interact | ActionType::InteractAt => {
// TODO: split this up
let entity = player.world().get_player_by_id(entity_id.0);
if let Some(entity) = entity {
let held = player.inventory.held_item();
let mut stack = held.lock().await;
server
.item_registry
.use_on_entity(&mut stack, player, entity)
}}
} else {
// Entity not found
send_cancellable! {{
server;
PlayerInteractUnknownEntityEvent::new(player, entity_id.0, action);
'after: {
if event.action == ActionType::Attack {
error!(
"Player id {} interacted with entity id {}, which was not found.",
player.entity_id(),
event.entity_id
);
self.kick(TextComponent::translate(
translation::MULTIPLAYER_DISCONNECT_INVALID_ENTITY_ATTACKED,
[],
))
.await;
}
}
}
}}
}
}
@@ -1511,7 +1534,7 @@ impl JavaClient {
pub async fn handle_use_item_on(
&self,
player: &Player,
player: &Arc<Player>,
use_item_on: SUseItemOn,
server: &Arc<Server>,
) -> Result<(), BlockPlacingError> {
@@ -2009,7 +2032,7 @@ impl JavaClient {
#[expect(clippy::too_many_lines)]
async fn run_is_block_place(
&self,
player: &Player,
player: &Arc<Player>,
block: &'static Block,
server: &Server,
use_item_on: SUseItemOn,
@@ -2165,6 +2188,13 @@ impl JavaClient {
}
}
let event =
BlockPlaceEvent::new(player.clone(), block, clicked_block, final_block_pos, true);
let event = server.plugin_manager.fire::<BlockPlaceEvent>(event).await;
if event.cancelled {
return Ok(false);
}
let _replaced_id = world
.set_block_state(&final_block_pos, new_state, BlockFlags::NOTIFY_ALL)
.await;

View File

@@ -1,5 +1,6 @@
use pumpkin_data::Block;
use pumpkin_macros::{Event, cancellable};
use pumpkin_util::math::position::BlockPos;
use std::sync::Arc;
use crate::entity::player::Player;
@@ -22,10 +23,33 @@ pub struct BlockPlaceEvent {
/// The block that the new block is being placed against.
pub block_placed_against: &'static Block,
/// The position where the block is being placed.
pub block_position: BlockPos,
/// A boolean indicating whether the player can build.
pub can_build: bool,
}
impl BlockPlaceEvent {
#[must_use]
pub const fn new(
player: Arc<Player>,
block_placed: &'static Block,
block_placed_against: &'static Block,
block_position: BlockPos,
can_build: bool,
) -> Self {
Self {
player,
block_placed,
block_placed_against,
block_position,
can_build,
cancelled: false,
}
}
}
impl BlockEvent for BlockPlaceEvent {
fn get_block(&self) -> &Block {
self.block_placed

View File

@@ -3,7 +3,9 @@ pub mod player_chat;
pub mod player_command_send;
pub mod player_custom_payload;
pub mod player_gamemode_change;
pub mod player_interact_entity_event;
pub mod player_interact_event;
pub mod player_interact_unknown_entity_event;
pub mod player_join;
pub mod player_leave;
pub mod player_login;

View File

@@ -0,0 +1,58 @@
use std::sync::Arc;
use crate::entity::EntityBase;
use crate::entity::player::Player;
use pumpkin_macros::{Event, cancellable};
use pumpkin_protocol::java::server::play::ActionType;
use pumpkin_util::math::vector3::Vector3;
use super::PlayerEvent;
/// Event that is triggered when a player interacts with an entity.
///
/// This event is fired for all entity interaction types: interact (right-click),
/// attack (left-click), and interact-at (right-click at specific position).
/// It can be cancelled to prevent the default interaction behavior.
#[cancellable]
#[derive(Event, Clone)]
pub struct PlayerInteractEntityEvent {
/// The player who performed the interaction.
pub player: Arc<Player>,
/// The entity that was interacted with.
pub target: Arc<dyn EntityBase>,
/// The type of interaction (Interact, Attack, or `InteractAt`).
pub action: ActionType,
/// The position on the entity that was clicked (only for `InteractAt`).
pub target_position: Option<Vector3<f32>>,
/// Whether the player was sneaking during the interaction.
pub sneaking: bool,
}
impl PlayerInteractEntityEvent {
pub fn new(
player: &Arc<Player>,
target: Arc<dyn EntityBase>,
action: ActionType,
target_position: Option<Vector3<f32>>,
sneaking: bool,
) -> Self {
Self {
player: Arc::clone(player),
target,
action,
target_position,
sneaking,
cancelled: false,
}
}
}
impl PlayerEvent for PlayerInteractEntityEvent {
fn get_player(&self) -> &Arc<Player> {
&self.player
}
}

View File

@@ -0,0 +1,41 @@
use std::sync::Arc;
use crate::entity::player::Player;
use pumpkin_macros::{Event, cancellable};
use pumpkin_protocol::java::server::play::ActionType;
use super::PlayerEvent;
/// Event that is triggered when a player interacts with an entity that was not found in the world.
///
/// This can occur when the target entity has been removed or is otherwise unknown to the server.
/// It can be cancelled to prevent the default behavior (e.g., kicking the player).
#[cancellable]
#[derive(Event, Clone)]
pub struct PlayerInteractUnknownEntityEvent {
/// The player who performed the interaction.
pub player: Arc<Player>,
/// The entity ID that was targeted.
pub entity_id: i32,
/// The type of interaction (Interact, Attack, or `InteractAt`).
pub action: ActionType,
}
impl PlayerInteractUnknownEntityEvent {
pub fn new(player: &Arc<Player>, entity_id: i32, action: ActionType) -> Self {
Self {
player: Arc::clone(player),
entity_id,
action,
cancelled: false,
}
}
}
impl PlayerEvent for PlayerInteractUnknownEntityEvent {
fn get_player(&self) -> &Arc<Player> {
&self.player
}
}