mirror of
https://github.com/Pumpkin-MC/Pumpkin.git
synced 2026-08-31 08:22:33 +00:00
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:
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user