use pumpkin_protocol::bedrock::server::text::SText; use pumpkin_util::{Hand, PermissionLvl}; use rsa::pkcs1v15::{Signature as RsaPkcs1v15Signature, VerifyingKey}; use rsa::signature::Verifier; use sha1::Sha1; use std::num::NonZeroU8; use std::sync::Arc; use std::sync::atomic::{AtomicU32, Ordering}; use std::time::{SystemTime, UNIX_EPOCH}; use thiserror::Error; use tracing::{Level, debug, error, info, trace, warn}; use crate::block::BlockHitResult; use crate::block::registry::BlockActionResult; use crate::block::{self}; use crate::entity::EntityBase; use crate::entity::equipment_break_status; use crate::entity::player::statistics::{CustomStatistic, StatisticCategory}; use crate::entity::player::{ChatMode, ChatSession, MINE_BLOCK_EXHAUSTION, Player}; use crate::error::PumpkinError; use crate::log_at_level; use crate::net::PlayerConfig; use crate::net::java::JavaClient; use crate::plugin::player::changed_main_hand::PlayerChangedMainHandEvent; use crate::plugin::player::fish::{PlayerFishEvent, PlayerFishState}; use crate::plugin::player::item_held::PlayerItemHeldEvent; 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::plugin::player::player_toggle_flight_event::PlayerToggleFlightEvent; use crate::plugin::player::player_toggle_sneak_event::PlayerToggleSneakEvent; use crate::block::entities::command_block::CommandBlockEntity; use crate::block::entities::jigsaw_block::JigsawBlockEntity; use crate::block::entities::sign::SignBlockEntity; use crate::plugin::player::player_toggle_sprint_event::PlayerToggleSprintEvent; use crate::server::{Server, seasonal_events}; use crate::world::{BlockBreakingProgress, World, chunker}; use pumpkin_data::block_properties::{BlockProperties, CommandBlockLikeProperties}; use pumpkin_data::data_component::DataComponent; use pumpkin_data::data_component_impl::{ BlocksAttacksImpl, ConsumableImpl, DataComponentImpl, EquipmentSlot, EquippableImpl, FoodImpl, WritableBookContentImpl, WrittenBookContentImpl, }; use pumpkin_data::item::Item; use pumpkin_data::item_stack::ItemStack; use pumpkin_data::sound::{Sound, SoundCategory}; use pumpkin_data::{Advancement, Block, BlockDirection, translation}; use pumpkin_inventory::InventoryError; use pumpkin_inventory::merchant::merchant_screen_handler::MerchantScreenHandler; use pumpkin_inventory::player::player_inventory::PlayerInventory; use pumpkin_inventory::screen_handler::{InventoryPlayer, ScreenHandler}; use pumpkin_protocol::bedrock::client::CMovePlayer; use pumpkin_protocol::codec::var_int::VarInt; use pumpkin_protocol::codec::var_ulong::VarULong; use pumpkin_protocol::java::client::play::{ CBlockUpdate, CCommandSuggestions, CEntityPositionSync, CHeadRot, COpenSignEditor, CPingResponse, CPlayerInfoUpdate, CPlayerPosition, CSetCamera, CSetSelectedSlot, CSystemChatMessage, CUpdateEntityPos, CUpdateEntityPosRot, CUpdateEntityRot, InitChat, PlayerAction, }; use pumpkin_protocol::java::server::play::{ Action, ActionType, CommandBlockMode, FLAG_ON_GROUND, SAttack, SBundleItemSelected, SChangeGameMode, SChatCommand, SChatMessage, SChunkBatch, SClientCommand, SClientInformationPlay, SCloseContainer, SCommandSuggestion, SConfirmTeleport, SCookieResponse as SPCookieResponse, SEditBook, SInteract, SJigsawGenerate, SKeepAlive, SMoveVehicle, SPaddleBoat, SPickItemFromBlock, SPickItemFromEntity, SPlaceRecipe, SPlayPingRequest, SPlayerAbilities, SPlayerAction, SPlayerCommand, SPlayerInput, SPlayerPosition, SPlayerPositionRotation, SPlayerRotation, SPlayerSession, SRecipeBookChangeSettings, SRecipeBookSeenRecipe, SSeenAdvancement, SSelectTrade, SSetCommandBlock, SSetCreativeSlot, SSetHeldItem, SSetJigsawBlock, SSetPlayerGround, SSetTestBlock, SSwingArm, STeleportToEntity, STestInstanceBlockAction, SUpdateSign, SUseItem, SUseItemOn, Status, }; use pumpkin_util::math::vector3::Vector3; use pumpkin_util::math::{polynomial_rolling_hash, position::BlockPos, wrap_degrees}; use pumpkin_util::{GameMode, text::TextComponent}; use pumpkin_world::generation::structure::structures::jigsaw::JigsawJointType; use pumpkin_world::world::BlockFlags; use tokio::sync::Mutex; /// In secure chat mode, Player will be kicked if they send a chat message with a timestamp that is older than this (in ms) /// Vanilla: 2 minutes const CHAT_MESSAGE_MAX_AGE: i64 = 1000 * 60 * 2; #[derive(Debug, Error)] pub enum BlockPlacingError { BlockOutOfReach, InvalidHand, InvalidBlockFace, BlockOutOfWorld, InvalidGamemode, } impl std::fmt::Display for BlockPlacingError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{self:?}") } } impl PumpkinError for BlockPlacingError { fn is_kick(&self) -> bool { match self { Self::BlockOutOfReach | Self::BlockOutOfWorld | Self::InvalidGamemode => false, Self::InvalidBlockFace | Self::InvalidHand => true, } } fn severity(&self) -> Level { match self { Self::BlockOutOfWorld | Self::InvalidGamemode => Level::TRACE, Self::BlockOutOfReach | Self::InvalidBlockFace | Self::InvalidHand => Level::WARN, } } fn client_kick_reason(&self) -> Option { match self { Self::BlockOutOfReach | Self::BlockOutOfWorld | Self::InvalidGamemode => None, Self::InvalidBlockFace => Some("Invalid block face".into()), Self::InvalidHand => Some("Invalid hand".into()), } } } #[derive(Debug, Error)] pub enum ChatError { #[error("sent an oversized message")] OversizedMessage, #[error("sent a message with illegal characters")] IllegalCharacters, #[error("sent a chat with invalid/no signature")] UnsignedChat, #[error("has too many unacknowledged chats queued")] TooManyPendingChats, #[error("sent a chat that couldn't be validated")] ChatValidationFailed, #[error("sent a chat with an out of order timestamp")] OutOfOrderChat, #[error("has an expired public key")] ExpiredPublicKey, #[error("attempted to initialize a session with an invalid public key")] InvalidPublicKey, } impl PumpkinError for ChatError { fn is_kick(&self) -> bool { true } fn severity(&self) -> Level { Level::WARN } fn client_kick_reason(&self) -> Option { match self { Self::OversizedMessage => Some("Chat message too long".into()), Self::IllegalCharacters => Some( TextComponent::translate_cross( translation::java::MULTIPLAYER_DISCONNECT_ILLEGAL_CHARACTERS, translation::java::MULTIPLAYER_DISCONNECT_ILLEGAL_CHARACTERS, [], ) .get_text(), ), Self::UnsignedChat => Some( TextComponent::translate_cross( translation::java::MULTIPLAYER_DISCONNECT_UNSIGNED_CHAT, translation::java::MULTIPLAYER_DISCONNECT_UNSIGNED_CHAT, [], ) .get_text(), ), Self::TooManyPendingChats => Some( TextComponent::translate_cross( translation::java::MULTIPLAYER_DISCONNECT_TOO_MANY_PENDING_CHATS, translation::java::MULTIPLAYER_DISCONNECT_TOO_MANY_PENDING_CHATS, [], ) .get_text(), ), Self::ChatValidationFailed => Some( TextComponent::translate_cross( translation::java::MULTIPLAYER_DISCONNECT_CHAT_VALIDATION_FAILED, translation::java::MULTIPLAYER_DISCONNECT_CHAT_VALIDATION_FAILED, [], ) .get_text(), ), Self::OutOfOrderChat => Some( TextComponent::translate_cross( translation::java::MULTIPLAYER_DISCONNECT_OUT_OF_ORDER_CHAT, translation::java::MULTIPLAYER_DISCONNECT_OUT_OF_ORDER_CHAT, [], ) .get_text(), ), Self::ExpiredPublicKey => Some( TextComponent::translate_cross( translation::java::MULTIPLAYER_DISCONNECT_EXPIRED_PUBLIC_KEY, translation::java::MULTIPLAYER_DISCONNECT_EXPIRED_PUBLIC_KEY, [], ) .get_text(), ), Self::InvalidPublicKey => Some( TextComponent::translate_cross( translation::java::MULTIPLAYER_DISCONNECT_INVALID_PUBLIC_KEY_SIGNATURE, translation::java::MULTIPLAYER_DISCONNECT_INVALID_PUBLIC_KEY_SIGNATURE, [], ) .get_text(), ), } } } /// Handles all Play packets sent by a real player. /// NEVER TRUST THE CLIENT. HANDLE EVERY ERROR; UNWRAP/EXPECT ARE FORBIDDEN. impl JavaClient { pub async fn handle_confirm_teleport( &self, player: &Player, confirm_teleport: SConfirmTeleport, ) { let mut awaiting_teleport = player.awaiting_teleport.lock().await; if let Some((id, position)) = awaiting_teleport.as_ref() { if id == &confirm_teleport.teleport_id { // We should set the position now to what we requested in the teleport packet. // This may fix issues when the client sends the position while being teleported. player.get_entity().set_pos(*position); *awaiting_teleport = None; drop(awaiting_teleport); } else { drop(awaiting_teleport); self.kick(TextComponent::text("Wrong teleport id")).await; } } else { drop(awaiting_teleport); self.kick(TextComponent::text( "Send Teleport confirm, but we did not teleport", )) .await; } } pub async fn handle_change_game_mode( &self, player: &Arc, change_game_mode: SChangeGameMode, ) { if player.permission_lvl.load() >= PermissionLvl::Two { player.set_gamemode(change_game_mode.game_mode).await; let gamemode_string = format!("gameMode.{}", change_game_mode.game_mode.name()); player .send_system_message(&TextComponent::translate_cross( translation::java::COMMANDS_GAMEMODE_SUCCESS_SELF, translation::bedrock::COMMANDS_GAMEMODE_SUCCESS_SELF, [TextComponent::translate_cross( gamemode_string.clone(), gamemode_string, [], )], )) .await; } } const fn clamp_horizontal(pos: f64) -> f64 { pos.clamp(-3.0E7, 3.0E7) } const fn clamp_vertical(pos: f64) -> f64 { pos.clamp(-2.0E7, 2.0E7) } pub fn handle_player_loaded(player: &Player) { player.set_client_loaded(true); } /// Returns whether syncing the position was needed fn sync_position( player: &Arc, world: &World, pos: Vector3, last_pos: Vector3, yaw: f32, pitch: f32, on_ground: bool, ) -> bool { let delta = Vector3::new(pos.x - last_pos.x, pos.y - last_pos.y, pos.z - last_pos.z); let entity_id = player.entity_id(); // Teleport when more than 8 blocks (-8..=7.999755859375) if delta.length_squared() < 64.0 { return false; } // Sync position with all other players. world.broadcast_packet_except( &[player.gameprofile.id], &CEntityPositionSync::new( entity_id.into(), pos, Vector3::new(0.0, 0.0, 0.0), yaw, pitch, on_ground, ), ); true } #[expect(clippy::too_many_lines)] pub async fn handle_position( &self, player: &Arc, server: &Arc, packet: SPlayerPosition, ) { if !player.has_client_loaded() { return; } if player.get_entity().has_vehicle().await { return; } // Ignore movement packets while awaiting a teleport confirmation (vanilla behavior) if player.awaiting_teleport.lock().await.is_some() { return; } // y = feet Y let position = packet.position; if position.x.is_nan() || position.y.is_nan() || position.z.is_nan() { self.kick(TextComponent::translate_cross( translation::java::MULTIPLAYER_DISCONNECT_INVALID_PLAYER_MOVEMENT, translation::java::MULTIPLAYER_DISCONNECT_INVALID_PLAYER_MOVEMENT, [], )) .await; return; } let position = Vector3::new( Self::clamp_horizontal(position.x), Self::clamp_vertical(position.y), Self::clamp_horizontal(position.z), ); send_cancellable! {{ server; PlayerMoveEvent { player: player.clone(), from: player.get_entity().pos.load(), to: position, cancelled: false, }; 'after: { let pos = event.to; let entity = &player.get_entity(); let last_pos = entity.pos.load(); player.get_entity().set_pos(pos); let distance = last_pos.squared_distance_to_vec(&pos).sqrt(); let cm = (distance * 100.0) as i32; if cm > 0 { let stat = player.get_movement_statistic().await; player .increment_stat(StatisticCategory::Custom, stat as i32, cm) .await; } let height_difference = pos.y - last_pos.y; if entity.on_ground.load(Ordering::Relaxed) && packet.collision & FLAG_ON_GROUND == 0 && height_difference > 0.0 { player.jump().await; } let new_on_ground = packet.collision & FLAG_ON_GROUND != 0; entity.on_ground.store(new_on_ground, Ordering::Relaxed); if new_on_ground && entity.is_fall_flying() { entity.set_fall_flying(false).await; } let world = &player.world(); // TODO: Warn when player moves to quickly if !Self::sync_position(player, world, pos, last_pos, entity.yaw.load(), entity.pitch.load(), packet.collision & FLAG_ON_GROUND != 0) { // Send the new position to all other players. world.broadcast_packet_except_editioned_sync( &[player.gameprofile.id], &CUpdateEntityPos::new( player.entity_id().into(), Vector3::new( pos.x.mul_add(4096.0, -(last_pos.x * 4096.0)) as i16, pos.y.mul_add(4096.0, -(last_pos.y * 4096.0)) as i16, pos.z.mul_add(4096.0, -(last_pos.z * 4096.0)) as i16, ), packet.collision & FLAG_ON_GROUND != 0, ), &CMovePlayer::new( VarULong(player.entity_id() as u64), Vector3::new(pos.x as f32, pos.y as f32 + player.get_entity().entity_type.eye_height, pos.z as f32), entity.pitch.load(), entity.yaw.load(), entity.yaw.load(), CMovePlayer::MODE_NORMAL, (packet.collision & FLAG_ON_GROUND) != 0, VarULong(0), 0, 0, VarULong(0), ), ); } // Only process fall damage if player is alive if !player.abilities.lock().await.flying && player.living_entity.health.load() > 0.0 && !player.living_entity.dead.load(Ordering::Relaxed) { player.living_entity .fall( player.clone(), height_difference, packet.collision & FLAG_ON_GROUND != 0, player.gamemode.load() == GameMode::Creative, ) .await; } chunker::update_position(player).await; let delta = Vector3::new( pos.x - last_pos.x, pos.y - last_pos.y, pos.z - last_pos.z, ); // Only update idle timeout if there's actual movement (vanilla threshold) if delta.length_squared() > 1.0E-5 { player.update_last_action_time(); } player.progress_motion(delta).await; } 'cancelled: { self.force_tp(player, player.get_entity().pos.load()).await; } }} } #[expect(clippy::too_many_lines)] pub async fn handle_position_rotation( &self, player: &Arc, server: &Arc, packet: SPlayerPositionRotation, ) { if !player.has_client_loaded() { return; } if player.get_entity().has_vehicle().await { return; } // Ignore movement packets while awaiting a teleport confirmation (vanilla behavior) if player.awaiting_teleport.lock().await.is_some() { return; } // y = feet Y let position = packet.position; if !position.x.is_finite() || !position.y.is_finite() || !position.z.is_finite() || !packet.yaw.is_finite() || !packet.pitch.is_finite() { self.kick(TextComponent::translate_cross( translation::java::MULTIPLAYER_DISCONNECT_INVALID_PLAYER_MOVEMENT, translation::java::MULTIPLAYER_DISCONNECT_INVALID_PLAYER_MOVEMENT, [], )) .await; return; } let position = Vector3::new( Self::clamp_horizontal(position.x), Self::clamp_vertical(position.y), Self::clamp_horizontal(position.z), ); send_cancellable! {{ server; PlayerMoveEvent::new( player.clone(), player.get_entity().pos.load(), position, ); 'after: { let pos = event.to; let entity = &player.get_entity(); let last_pos = entity.pos.load(); player.get_entity().set_pos(pos); let distance = last_pos.squared_distance_to_vec(&pos).sqrt(); let cm = (distance * 100.0) as i32; if cm > 0 { let stat = player.get_movement_statistic().await; player .increment_stat(StatisticCategory::Custom, stat as i32, cm) .await; } let height_difference = pos.y - last_pos.y; if entity.on_ground.load(Ordering::Relaxed) && (packet.collision & FLAG_ON_GROUND) != 0 && height_difference > 0.0 { player.jump().await; } entity .on_ground .store((packet.collision & FLAG_ON_GROUND) != 0, Ordering::Relaxed); entity.set_rotation(wrap_degrees(packet.yaw) % 360.0, wrap_degrees(packet.pitch)); let entity_id = entity.entity_id; let yaw = (entity.yaw.load() * 256.0 / 360.0).rem_euclid(256.0); let pitch = (entity.pitch.load() * 256.0 / 360.0).rem_euclid(256.0); // let head_yaw = (entity.head_yaw * 256.0 / 360.0).floor(); let world = entity.world.load_full(); // TODO: Warn when player moves to quickly if !Self:: sync_position(player, &world, pos, last_pos, yaw, pitch, (packet.collision & FLAG_ON_GROUND) != 0) { // Send the new position to all other players. world.broadcast_packet_except_editioned_sync( &[player.gameprofile.id], &CUpdateEntityPosRot::new( entity_id.into(), Vector3::new( pos.x.mul_add(4096.0, -(last_pos.x * 4096.0)) as i16, pos.y.mul_add(4096.0, -(last_pos.y * 4096.0)) as i16, pos.z.mul_add(4096.0, -(last_pos.z * 4096.0)) as i16, ), yaw as u8, pitch as u8, (packet.collision & FLAG_ON_GROUND) != 0, ), &CMovePlayer::new( VarULong(entity_id as u64), Vector3::new(pos.x as f32, pos.y as f32 + player.get_entity().entity_type.eye_height, pos.z as f32), entity.pitch.load(), entity.yaw.load(), entity.yaw.load(), CMovePlayer::MODE_NORMAL, (packet.collision & FLAG_ON_GROUND) != 0, VarULong(0), 0, 0, VarULong(0), ), ); } world .broadcast_packet_except( &[player.gameprofile.id], &CHeadRot::new(entity_id.into(), yaw as u8), ) ; // Only process fall damage if player is alive if !player.abilities.lock().await.flying && player.living_entity.health.load() > 0.0 && !player.living_entity.dead.load(Ordering::Relaxed) { player.living_entity .fall( player.clone(), height_difference, (packet.collision & FLAG_ON_GROUND) != 0, player.gamemode.load() == GameMode::Creative, ) .await; } chunker::update_position(player).await; let delta = Vector3::new( pos.x - last_pos.x, pos.y - last_pos.y, pos.z - last_pos.z, ); // Only update idle timeout if there's actual movement (vanilla threshold) if delta.length_squared() > 1.0E-5 { player.update_last_action_time(); } player.progress_motion(delta).await; } 'cancelled: { self.force_tp(player, position).await; } }} } pub async fn force_tp(&self, player: &Arc, position: Vector3) { let teleport_id = player.teleport_id_count.fetch_add(1, Ordering::Relaxed) + 1; *player.awaiting_teleport.lock().await = Some((teleport_id.into(), position)); self.enqueue_packet(&CPlayerPosition::new( teleport_id.into(), player.get_entity().pos.load(), Vector3::new(0.0, 0.0, 0.0), player.get_entity().yaw.load(), player.get_entity().pitch.load(), Vec::new(), )) .await; } pub async fn handle_rotation(&self, player: &Player, rotation: SPlayerRotation) { if !player.has_client_loaded() { return; } if !rotation.yaw.is_finite() || !rotation.pitch.is_finite() { self.kick(TextComponent::translate_cross( translation::java::MULTIPLAYER_DISCONNECT_INVALID_PLAYER_MOVEMENT, translation::java::MULTIPLAYER_DISCONNECT_INVALID_PLAYER_MOVEMENT, [], )) .await; return; } let entity = &player.get_entity(); entity.on_ground.store(rotation.ground, Ordering::Relaxed); entity.set_rotation( wrap_degrees(rotation.yaw) % 360.0, wrap_degrees(rotation.pitch), ); // Send the new position to all other players. let entity_id = entity.entity_id; let yaw = (entity.yaw.load() * 256.0 / 360.0).rem_euclid(256.0); let pitch = (entity.pitch.load() * 256.0 / 360.0).rem_euclid(256.0); // let head_yaw = modulus(entity.head_yaw * 256.0 / 360.0, 256.0); let world = entity.world.load_full(); let je_packet = CUpdateEntityRot::new(entity_id.into(), yaw as u8, pitch as u8, rotation.ground); let pos = entity.pos.load(); let be_packet = CMovePlayer::new( VarULong(entity_id as u64), Vector3::new( pos.x as f32, pos.y as f32 + player.get_entity().entity_type.eye_height, pos.z as f32, ), entity.pitch.load(), entity.yaw.load(), entity.yaw.load(), CMovePlayer::MODE_ROTATION, rotation.ground, VarULong(0), 0, 0, VarULong(0), ); world.broadcast_packet_except_editioned_sync( &[player.gameprofile.id], &je_packet, &be_packet, ); let je_packet = CHeadRot::new(entity_id.into(), yaw as u8); world.broadcast_packet_except(&[player.gameprofile.id], &je_packet); } pub async fn handle_chat_command( &self, player: &Arc, server: &Arc, command: &SChatCommand<'_>, ) { player.update_last_action_time(); let player_clone = player.clone(); let server_clone = server.clone(); send_cancellable! {{ server; PlayerCommandSendEvent { player: player.clone(), command: command.command.to_string(), cancelled: false }; 'after: { let command = event.command; let command_clone = command.clone(); // Some commands can take a long time to execute. If they do, they block packet processing for the player. // That's why we will spawn a task instead. server.spawn_task(async move { let dispatcher = server_clone.command_dispatcher.read().await; dispatcher.handle_command( &player_clone.get_command_source(&server_clone).await, &command_clone ).await; }); if server.advanced_config.commands.log_console { info!( "Player ({}): executed command /{}", player.gameprofile.name, command ); } } }} } pub fn handle_player_ground(&self, player: &Player, ground: &SSetPlayerGround) { player .living_entity .entity .on_ground .store(ground.on_ground, Ordering::Relaxed); } pub async fn handle_pick_item_from_block( &self, player: &Arc, pick_item: SPickItemFromBlock, ) { if !player.can_interact_with_block_at(&pick_item.pos, 1.0) { return; } let world = player.world(); let block = world.get_block(&pick_item.pos); if block.item_id == 0 { // Invalid block id (blocks such as tall seagrass) return; } let Some(item) = Item::from_id(block.item_id) else { return; }; let stack = ItemStack::new(1, item); let slot_with_stack = player.inventory().get_slot_with_stack(&stack).await; if slot_with_stack != -1 { if PlayerInventory::is_valid_hotbar_index(slot_with_stack as usize) { player.inventory.set_selected_slot(slot_with_stack as u8); } else { player .inventory .swap_slot_with_hotbar(slot_with_stack as usize) .await; } } else if player.gamemode.load() == GameMode::Creative { player.inventory.swap_stack_with_hotbar(stack).await; } player .client .enqueue_packet(&CSetSelectedSlot::new( player.inventory.get_selected_slot() as i8 )) .await; player .player_screen_handler .lock() .await .send_content_updates() .await; } pub async fn handle_pick_item_from_entity( &self, player: &Arc, pick_item: SPickItemFromEntity, ) { use pumpkin_data::entity::{entity_from_egg, spawn_egg_ids}; let world = player.world(); let Some(target) = world.get_entity_by_id(pick_item.id.0) else { return; }; let p_eye = player.get_entity().get_eye_pos(); let t_eye = target.get_eye_pos(); let dx = p_eye.x - t_eye.x; let dy = p_eye.y - t_eye.y; let dz = p_eye.z - t_eye.z; if dx * dx + dy * dy + dz * dz > 64.0 { return; } let target_type_id = target.get_entity().entity_type.id; let mut found_egg: Option = None; for &egg_id in &spawn_egg_ids() { if let Some(et) = entity_from_egg(egg_id) && et.id == target_type_id { found_egg = Some(egg_id); break; } } if let Some(item) = found_egg.and_then(Item::from_id) { let stack = ItemStack::new(1, item); let slot_with_stack = player.inventory().get_slot_with_stack(&stack).await; if slot_with_stack != -1 { if PlayerInventory::is_valid_hotbar_index(slot_with_stack as usize) { player.inventory.set_selected_slot(slot_with_stack as u8); } else { player .inventory .swap_slot_with_hotbar(slot_with_stack as usize) .await; } } else if player.gamemode.load() == GameMode::Creative { player.inventory.swap_stack_with_hotbar(stack).await; } player .client .enqueue_packet(&CSetSelectedSlot::new( player.inventory.get_selected_slot() as i8 )) .await; player .player_screen_handler .lock() .await .send_content_updates() .await; } } pub async fn handle_set_command_block( &self, player: &Arc, command: SSetCommandBlock<'_>, ) { if !player.is_creative() { return; } if player.permission_lvl.load() < PermissionLvl::Two { return; } let pos = command.pos; if let Some(block_entity) = player.world().get_block_entity(&pos) { if block_entity.resource_location() != CommandBlockEntity::ID { warn!("Client tried to change Command block but not Command block entity found"); return; } let Ok(command_block_mode) = CommandBlockMode::try_from(command.mode) else { self.kick(TextComponent::text("Invalid Command block mode")) .await; return; }; let block = player.world().get_block(&pos); let old_state_id = player.world().get_block_state_id(&pos); let mut props = CommandBlockLikeProperties::from_state_id(old_state_id, block); let block_type = match command_block_mode { CommandBlockMode::Chain => Block::CHAIN_COMMAND_BLOCK, CommandBlockMode::Repeating => Block::REPEATING_COMMAND_BLOCK, CommandBlockMode::Impulse => Block::COMMAND_BLOCK, }; let Some(old_command_block) = block_entity.as_any().downcast_ref::() else { return; }; props.conditional = command.flags & 0x2 != 0; let new_state_id = props.to_state_id(&block_type); player .world() .set_block_state( &command.pos, new_state_id, BlockFlags::SKIP_BLOCK_ADDED_CALLBACK, ) .await; let mut cmd = command.command; if cmd.starts_with('/') { cmd = &cmd[1..]; } let command_block = CommandBlockEntity { position: pos, powered: old_command_block.powered.load(Ordering::SeqCst).into(), condition_met: old_command_block .condition_met .load(Ordering::SeqCst) .into(), auto: (command.flags & 0x4 != 0).into(), dirty: old_command_block.dirty.load(Ordering::SeqCst).into(), command: Mutex::new(cmd.to_string()), last_output: old_command_block.last_output.lock().await.clone().into(), track_output: (command.flags & 0x1 != 0).into(), success_count: AtomicU32::new(0), }; player.world().add_block_entity(Arc::new(command_block)); player .send_system_message(&TextComponent::text(format!( "Command set: {}", command.command ))) .await; // The 0x4 flag means always active if command.flags & 0x4 != 0 && block_type != Block::CHAIN_COMMAND_BLOCK { player.world().schedule_block_tick( &block_type, pos, 1, pumpkin_world::tick::TickPriority::Normal, ); } } } pub async fn handle_set_jigsaw_block(&self, player: &Arc, jigsaw: SSetJigsawBlock<'_>) { if !player.is_creative() { return; } if player.permission_lvl.load() < PermissionLvl::Two { return; } let pos = jigsaw.pos; if let Some(block_entity) = player.world().get_block_entity(&pos) { if block_entity.resource_location() != JigsawBlockEntity::ID { warn!("Client tried to change Jigsaw block but not Jigsaw block entity found"); return; } let Some(jigsaw_block) = block_entity.as_any().downcast_ref::() else { return; }; *jigsaw_block.name.lock().await = jigsaw.name.to_string(); *jigsaw_block.target.lock().await = jigsaw.target.to_string(); *jigsaw_block.pool.lock().await = jigsaw.pool.to_string(); *jigsaw_block.final_state.lock().await = jigsaw.final_state.to_string(); *jigsaw_block.joint.lock().await = JigsawJointType::from_str(jigsaw.joint); jigsaw_block .selection_priority .store(jigsaw.selection_priority.0, Ordering::SeqCst); jigsaw_block .placement_priority .store(jigsaw.placement_priority.0, Ordering::SeqCst); player.world().update_block_entity(&block_entity); } } pub async fn handle_jigsaw_generate(&self, player: &Arc, generate: SJigsawGenerate) { if !player.is_creative() { return; } if player.permission_lvl.load() < PermissionLvl::Two { return; } let pos = generate.pos; if let Some(block_entity) = player.world().get_block_entity(&pos) && let Some(jigsaw_block) = block_entity.as_any().downcast_ref::() { jigsaw_block .generate(&player.world(), generate.levels.0, generate.keep_jigsaws) .await; } } pub async fn handle_player_command( &self, player: &Arc, command: SPlayerCommand, server: &Arc, ) { if command.entity_id != player.entity_id().into() { return; } if !player.has_client_loaded() { return; } player.update_last_action_time(); let entity = &player.get_entity(); match command.action { Action::StartSprinting => { if !entity.is_sprinting() { send_cancellable! {{ server; PlayerToggleSprintEvent::new(player.clone(), true); 'after: { player.get_entity().set_sprinting(event.is_sprinting).await; } }} } } Action::StopSprinting => { if entity.is_sprinting() { send_cancellable! {{ server; PlayerToggleSprintEvent::new(player.clone(), false); 'after: { player.get_entity().set_sprinting(event.is_sprinting).await; } }} } } Action::LeaveBed => player.wake_up().await, Action::StartHorseJump | Action::StopHorseJump | Action::OpenVehicleInventory => { debug!("todo"); } Action::StartFlyingElytra => { let fall_flying = entity.check_fall_flying(); if entity.is_fall_flying() != fall_flying { entity.set_fall_flying(fall_flying).await; } } // <= 1.21.5 Action::StartSneaking | Action::StopSneaking => { self.handle_player_input( player, SPlayerInput { input: SPlayerInput::SNEAK, }, server, ) .await; } } } pub async fn handle_player_input( &self, player: &Arc, input: SPlayerInput, server: &Arc, ) { player.last_input.store(input.input, Ordering::Relaxed); let sneak = input.input & SPlayerInput::SNEAK != 0; if sneak && player.gamemode.load() == GameMode::Spectator && player.camera_target_id.load().is_some() { player.camera_target_id.store(None); player .client .send_packet_now(&CSetCamera::new(player.entity_id().into())) .await; } if player.get_entity().is_sneaking() != sneak { send_cancellable! {{ server; PlayerToggleSneakEvent::new(player.clone(), sneak); 'after: { player.get_entity().set_sneaking(event.is_sneaking).await; if event.is_sneaking { let vehicle = player.get_entity().vehicle.lock().await.clone(); if let Some(vehicle) = vehicle { vehicle .get_entity() .remove_passenger(player.entity_id()) .await; } } } }} } else if sneak { let vehicle = player.get_entity().vehicle.lock().await.clone(); if let Some(vehicle) = vehicle { vehicle .get_entity() .remove_passenger(player.entity_id()) .await; } } } pub async fn handle_move_vehicle(&self, player: &Arc, packet: SMoveVehicle) { let entity = player.get_entity(); let pos = Vector3::new(packet.x, packet.y, packet.z); let vehicle = entity.vehicle.lock().await; if let Some(vehicle) = vehicle.as_ref() { let vehicle_entity = vehicle.get_entity(); vehicle_entity.set_pos(pos); vehicle_entity.set_rotation(packet.yaw, packet.pitch); } drop(vehicle); entity.set_pos(pos); chunker::update_position(player).await; } pub async fn handle_paddle_boat(&self, player: &Arc, packet: SPaddleBoat) { let vehicle = player.get_entity().vehicle.lock().await.clone(); if let Some(vehicle) = vehicle { vehicle .set_paddle_state(packet.left_paddle, packet.right_paddle) .await; } } #[allow(clippy::unused_async)] pub async fn handle_recipe_book_change_settings( &self, _player: &Arc, _packet: SRecipeBookChangeSettings, ) { // Client is updating its recipe book filter/open state; no server action needed. } #[allow(clippy::unused_async)] pub async fn handle_recipe_book_seen_recipe( &self, _player: &Arc, _packet: SRecipeBookSeenRecipe, ) { // Client acknowledged a recipe display; no server action needed. } #[allow(clippy::too_many_lines)] pub async fn handle_place_recipe( &self, server: &Arc, player: &Arc, packet: SPlaceRecipe, ) { use super::recipe_helper::{ GenericIngredient, compute_biggest_craftable, take_n_ingredient, }; use crate::server::recipe::DynamicRecipe; use pumpkin_data::recipes::{CraftingRecipeTypes, RECIPES_COOKING, RECIPES_CRAFTING}; use pumpkin_data::screen::WindowType; use pumpkin_inventory::crafting::recipe_provider::RecipeProvider; let target_id = packet.recipe_display_id.0 as usize; let use_max = packet.use_max_items; // Count crafting display IDs. let crafting_display_count = RECIPES_CRAFTING .iter() .filter(|r| { !matches!( r, CraftingRecipeTypes::CraftingSpecial | CraftingRecipeTypes::CraftingDecoratedPot { .. } ) }) .count(); let cooking_display_count = RECIPES_COOKING.len(); let dynamic_recipes = server.recipe_manager.get_dynamic_recipes().await; let (grid_width, crafting_inv) = { let screen_handler_arc = player.current_screen_handler.lock().await.clone(); let handler = screen_handler_arc.lock().await; let grid_width: usize = match handler.window_type() { Some(WindowType::Crafting) => 3, None => 2, // player inventory 2x2 _ => return, }; (grid_width, handler.get_behaviour().slots[1].get_inventory()) }; let grid_size = grid_width * grid_width; let mut ingredient_slots: Vec>> = vec![None; grid_size]; if target_id < crafting_display_count { // Crafting recipe let mut counter = 0usize; let recipe = RECIPES_CRAFTING.iter().find(|r| { if matches!( r, CraftingRecipeTypes::CraftingSpecial | CraftingRecipeTypes::CraftingDecoratedPot { .. } ) { return false; } let found = counter == target_id; counter += 1; found }); let Some(recipe) = recipe else { return }; match recipe { CraftingRecipeTypes::CraftingShaped { pattern, key, .. } => { for (row, row_str) in pattern.iter().enumerate() { for (col, ch) in row_str.chars().enumerate() { if ch != ' ' && let Some(ing) = key.iter().find_map(|(k, v)| (*k == ch).then_some(v)) && row * grid_width + col < grid_size { ingredient_slots[row * grid_width + col] = Some(GenericIngredient::Vanilla(ing)); } } } } CraftingRecipeTypes::CraftingShapeless { ingredients, .. } => { for (i, ing) in ingredients.iter().enumerate().take(grid_size) { ingredient_slots[i] = Some(GenericIngredient::Vanilla(ing)); } } CraftingRecipeTypes::CraftingTransmute { input, material, .. } => { if grid_size >= 2 { ingredient_slots[0] = Some(GenericIngredient::Vanilla(input)); ingredient_slots[1] = Some(GenericIngredient::Vanilla(material)); } } _ => return, } } else if target_id < crafting_display_count + cooking_display_count { // TODO: cooking recipes return; } else { let dynamic_id = target_id - crafting_display_count - cooking_display_count; let Some(DynamicRecipe::Crafting(crafting)) = dynamic_recipes.get(dynamic_id) else { return; }; match crafting { pumpkin_protocol::codec::recipe::OwnedCraftingRecipe::Shaped { pattern, key, .. } => { for (row, row_str) in pattern.iter().enumerate() { for (col, ch) in row_str.chars().enumerate() { if ch != ' ' && let Some((_, ing)) = key.iter().find(|(k, _)| *k == ch) && row * grid_width + col < grid_size { ingredient_slots[row * grid_width + col] = Some(GenericIngredient::Dynamic(ing)); } } } } pumpkin_protocol::codec::recipe::OwnedCraftingRecipe::Shapeless { ingredients, .. } => { for (i, ing) in ingredients.iter().enumerate().take(grid_size) { ingredient_slots[i] = Some(GenericIngredient::Dynamic(ing)); } } } } // Check if this exact recipe is already placed (determines stacking vs fresh fill). let recipe_matches = { let mut ok = true; for (idx, ing) in ingredient_slots.iter().enumerate() { let stack = crafting_inv.get_stack(idx).await; match ing { None => { if !stack.is_empty() { ok = false; break; } } Some(ingredient) => { if stack.is_empty() || !ingredient.match_item(stack.item) { ok = false; break; } } } } ok }; // Read minimum count from occupied slots before clearing (needed for stacking). let current_min = if recipe_matches && !use_max { let mut min = u8::MAX; for (idx, ing) in ingredient_slots.iter().enumerate() { if ing.is_some() { let stack = crafting_inv.get_stack(idx).await; if !stack.is_empty() { min = min.min(stack.item_count); } } } if min == u8::MAX { 0 } else { min } } else { 0 }; // Always clear the grid first, returning items to inventory. for i in 0..grid_size { let stack = crafting_inv.remove_stack(i).await; if !stack.is_empty() { player.inventory.offer(stack, false, player.as_ref()).await; } } // Determine how many of each ingredient to place per slot. let active_ingredients: Vec> = ingredient_slots.iter().flatten().copied().collect(); let amount_to_craft = if use_max { compute_biggest_craftable(&active_ingredients, &player.inventory).await } else if recipe_matches { current_min.saturating_add(1) } else { 1 }; if amount_to_craft == 0 { let screen_handler_arc = player.current_screen_handler.lock().await.clone(); screen_handler_arc.lock().await.send_content_updates().await; return; } // Fill each grid slot with exactly `amount_to_craft` matching items. for (idx, ing) in ingredient_slots.iter().enumerate() { let Some(ingredient) = ing else { continue }; let taken = take_n_ingredient(&player.inventory, ingredient, amount_to_craft).await; if !taken.is_empty() { crafting_inv.set_stack(idx, taken).await; } } let screen_handler_arc = player.current_screen_handler.lock().await.clone(); screen_handler_arc.lock().await.send_content_updates().await; } pub async fn handle_swing_arm( &self, server: &Arc, player: &Arc, swing_arm: SSwingArm, ) { player.update_last_action_time(); let Ok(hand) = Hand::try_from(swing_arm.hand.0) else { self.kick(TextComponent::text("Invalid hand")).await; return; }; let mut anim_event = crate::plugin::api::events::player::player_animation::PlayerAnimationEvent::new( player.clone(), match hand { Hand::Left => crate::plugin::api::events::player::player_animation::PlayerAnimationType::ArmSwingOff, Hand::Right => crate::plugin::api::events::player::player_animation::PlayerAnimationType::ArmSwingMain, }, ); server.plugin_manager.fire(server, &mut anim_event).await; if anim_event.cancelled { return; } let (yaw, pitch) = player.rotation(); let hit_result = player .world() .raycast( player.eye_position(), player .eye_position() .add(&(Vector3::rotation_vector(f64::from(pitch), f64::from(yaw)) * 4.5)), async |pos, world| { let block = world.get_block(pos); block != &Block::AIR && block != &Block::WATER && block != &Block::LAVA }, ) .await; let event = if let Some((hit_pos, _hit_dir)) = hit_result { PlayerInteractEvent::new( player, InteractAction::LeftClickBlock, player.world().get_block(&hit_pos), Some(hit_pos), ) } else { PlayerInteractEvent::new(player, InteractAction::LeftClickAir, &Block::AIR, None) }; send_cancellable! {{ &server; event; 'after: { player.swing_hand(hand, false).await; } }} } pub async fn handle_chat_message( &self, server: &Arc, player: &Arc, chat_message: SChatMessage<'_>, ) { player.update_last_action_time(); let gameprofile = &player.gameprofile; if let Err(err) = self .validate_chat_message(server, player, &chat_message) .await { log_at_level!( err.severity(), "{} (uuid {}) {}", gameprofile.name, gameprofile.id, err ); if err.is_kick() && let Some(reason) = err.client_kick_reason() { self.kick(TextComponent::text(reason)).await; } return; } send_cancellable! {{ server; PlayerChatEvent::new(player.clone(), chat_message.message.to_string(), vec![]); 'after: { info!(" {}: {}", gameprofile.name, event.message); let config = &server.advanced_config; let message = match seasonal_events::modify_chat_message(&event.message, config) { Some(m) => m, None => event.message.clone(), }; let decorated_message = TextComponent::chat_decorated( &config.chat.format, &gameprofile.name, &message, ); let entity = &player.get_entity(); let world = entity.world.load_full(); if server.basic_config.allow_chat_reports { world.broadcast_secure_player_chat(player, &chat_message, &decorated_message).await; } else { let je_packet = CSystemChatMessage::new( &decorated_message, false, ); let be_packet = SText::new( message, player.gameprofile.name.clone() ); world.broadcast_editioned(&je_packet, &be_packet).await; } } }} } /// Runs all vanilla checks for a valid chat message pub async fn validate_chat_message( &self, server: &Server, player: &Arc, chat_message: &SChatMessage<'_>, ) -> Result<(), ChatError> { // Check for oversized messages // If we're able to find the 257th UTF-16 character, the message is too big. if chat_message.message.encode_utf16().nth(256).is_some() { return Err(ChatError::OversizedMessage); } // Check for illegal characters if chat_message .message .chars() .any(|c| c == 'ยง' || c < ' ' || c == '\x7F') { return Err(ChatError::IllegalCharacters); } // These checks are only run in secure chat mode if server.basic_config.allow_chat_reports { // Check for unsigned chat if let Some(signature) = &chat_message.signature { if signature.len() != 256 { return Err(ChatError::UnsignedChat); // Signature is the wrong length } } else { return Err(ChatError::UnsignedChat); // There is no signature } let now = SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap_or_default() .as_millis() as i64; // Verify message timestamp if chat_message.timestamp > now || chat_message.timestamp < (now - CHAT_MESSAGE_MAX_AGE) { return Err(ChatError::OutOfOrderChat); } // Verify session expiry if player.chat_session.lock().await.expires_at < now { return Err(ChatError::ExpiredPublicKey); } // Validate previous signature checksum (new in 1.21.5) // The client can bypass this check by sending 0 if chat_message.checksum != 0 { let checksum = polynomial_rolling_hash(player.signature_cache.lock().await.last_seen.as_ref()); if checksum != chat_message.checksum { return Err(ChatError::ChatValidationFailed); } } } Ok(()) } pub async fn handle_chat_session_update( &self, player: &Arc, server: &Server, session: SPlayerSession, ) { // Keep the chat session default if we don't want reports if !server.basic_config.allow_chat_reports { return; } if let Err(err) = self.validate_chat_session(player, server, &session) { log_at_level!( err.severity(), "{} (uuid {}) {}", player.gameprofile.name, player.gameprofile.id, err ); if err.is_kick() && let Some(reason) = err.client_kick_reason() { self.kick(TextComponent::text(reason)).await; } return; } // Update the chat session fields *player.chat_session.lock().await = ChatSession::new( session.session_id, session.expires_at, session.public_key.clone(), session.key_signature.clone(), ); server.broadcast_packet_all(&CPlayerInfoUpdate::new( 0x02, &[pumpkin_protocol::java::client::play::Player { uuid: player.gameprofile.id, actions: &[PlayerAction::InitializeChat(Some(InitChat { session_id: session.session_id, expires_at: session.expires_at, public_key: session.public_key.clone(), signature: session.key_signature.clone(), }))], }], )); } /// Runs vanilla checks for a valid player session pub fn validate_chat_session( &self, player: &Player, server: &Server, session: &SPlayerSession, ) -> Result<(), ChatError> { // Verify session expiry let now = SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap_or_default() .as_millis() as i64; if session.expires_at < now { return Err(ChatError::InvalidPublicKey); } let key_signature = RsaPkcs1v15Signature::try_from(session.key_signature.as_ref()) .map_err(|_| ChatError::InvalidPublicKey)?; let mut signable = Vec::new(); signable.extend_from_slice(player.gameprofile.id.as_bytes()); signable.extend_from_slice(&session.expires_at.to_be_bytes()); signable.extend_from_slice(&session.public_key); let public_keys_guard = server.mojang_public_keys.load(); // Verify signature with RSA-SHA1 let is_valid = public_keys_guard.iter().any(|key| { let verifying_key = VerifyingKey::::new(key.clone()); verifying_key.verify(&signable, &key_signature).is_ok() }); // Verify that the signable is valid for any one of Mojang's public keys if !is_valid { return Err(ChatError::InvalidPublicKey); } Ok(()) } pub async fn handle_client_information( &self, server: &Arc, player: &Arc, client_information: SClientInformationPlay<'_>, ) { if let (Ok(main_hand), Ok(chat_mode)) = ( Hand::try_from(client_information.main_hand.0), ChatMode::try_from(client_information.chat_mode.0), ) { if client_information.view_distance <= 0 { self.kick(TextComponent::text( "Cannot have zero or negative view distance!", )) .await; return; } let (update_settings, update_watched, main_hand_changed) = { // 1. Load current snapshot let current_config = player.config.load(); // 2. Calculate if settings changed before we overwrite let main_hand_changed = current_config.main_hand != main_hand; let update_settings = main_hand_changed || current_config.skin_parts != client_information.skin_parts; let old_view_distance = current_config.view_distance; let new_view_distance_raw = client_information.view_distance as u8; let update_watched = if old_view_distance.get() == new_view_distance_raw { false } else { debug!( "Player {} ({}) updated their render distance: {} -> {}.", player.gameprofile.name, self.id, old_view_distance, new_view_distance_raw ); true }; // 3. Construct the new config // If view_distance is 0, we exit early (safe guard) let Some(new_view_distance) = NonZeroU8::new(new_view_distance_raw) else { return; }; let new_config = PlayerConfig { locale: client_information.locale.to_string(), view_distance: new_view_distance, chat_mode, chat_colors: client_information.chat_colors, skin_parts: client_information.skin_parts, main_hand, text_filtering: client_information.text_filtering, server_listing: client_information.server_listing, }; // 4. Atomically swap the new config into the player player.config.store(std::sync::Arc::new(new_config)); (update_settings, update_watched, main_hand_changed) }; if update_watched { chunker::update_position(player).await; } if main_hand_changed { let mut event = PlayerChangedMainHandEvent::new(player.clone(), main_hand); server.plugin_manager.fire(server, &mut event).await; } if update_settings { debug!( "Player {} ({}) updated their skin.", player.gameprofile.name, self.id, ); player.send_client_information(); } } else { self.kick(TextComponent::text("Invalid hand or chat type")) .await; } } pub async fn handle_client_status(&self, player: &Arc, client_status: SClientCommand) { player.update_last_action_time(); match client_status.action_id.0 { 0 => { // Perform respawn if player.living_entity.health.load() > 0.0 { return; } player.world().clone().respawn_player(player, false).await; { let screen_handler = player.current_screen_handler.lock().await; let mut screen_handler = screen_handler.lock().await; screen_handler.sync_state().await; }; // Restore abilities based on gamemode after respawn { let mut abilities = player.abilities.lock().await; abilities.set_for_gamemode(player.gamemode.load()); }; player.send_abilities_update().await; } 1 => { // Request stats player.send_stats().await; } _ => { self.kick(TextComponent::text("Invalid client status")) .await; } } } pub async fn handle_attack(&self, player: &Arc, attack: SAttack, server: &Arc) { if !player.has_client_loaded() { return; } player.update_last_action_time(); let entity_id = attack.entity_id; let player_entity = &player.get_entity(); let world = player_entity.world.load_full(); let config = &server.advanced_config.pvp; if !config.enabled { return; } if entity_id.0 == player.entity_id() { self.kick(TextComponent::translate_cross( translation::java::MULTIPLAYER_DISCONNECT_INVALID_ENTITY_ATTACKED, translation::java::MULTIPLAYER_DISCONNECT_INVALID_ENTITY_ATTACKED, [], )) .await; return; } let player_target = world.get_player_by_id(entity_id.0); let target: Option> = player_target .as_ref() .map(|p| Arc::clone(p) as Arc) .or_else(|| world.get_entity_by_id(entity_id.0)); let Some(target) = target else { self.kick(TextComponent::translate_cross( translation::java::MULTIPLAYER_DISCONNECT_INVALID_ENTITY_ATTACKED, translation::java::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(), ); return; } } player.attack(target).await; } #[expect(clippy::too_many_lines)] pub async fn handle_interact( &self, player: &Arc, interact: SInteract, server: &Arc, ) { if !player.has_client_loaded() { return; } player.update_last_action_time(); let entity_id = interact.entity_id; let sneaking = interact.sneaking; let player_entity = &player.get_entity(); if player_entity.is_sneaking() != sneaking { player_entity.set_sneaking(sneaking).await; } let Ok(action) = ActionType::try_from(interact.r#type.0) else { self.kick(TextComponent::text("Invalid action type")).await; 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> = player_target .as_ref() .map(|p| Arc::clone(p) as Arc) .or_else(|| world.get_entity_by_id(entity_id.0)); if let Some(target) = target { if player.gamemode.load() == GameMode::Spectator { player.camera_target_id.store(Some(entity_id.0)); player .client .send_packet_now(&CSetCamera::new(entity_id)) .await; return; } send_cancellable! {{ server; PlayerInteractEntityEvent::new( player, Arc::clone(&target), action.clone(), interact.target_position, sneaking, ); '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_cross(translation::java::MULTIPLAYER_DISCONNECT_INVALID_ENTITY_ATTACKED, translation::java::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(), ) ; return; } } player.attack(event.target).await; } ActionType::Interact | ActionType::InteractAt => { let mut stack = player.inventory().held_item().await; let target_entity = event.target.get_entity(); if target_entity.entity_type.resource_name == "zombie_villager" && stack.item.registry_key == "golden_apple" { player.trigger_advancement(crate::entity::player::advancement::trigger::AdvancementTrigger::CuredZombieVillager).await; } let interacted = event.target.interact(player, &mut stack).await; if !interacted { server .item_registry .use_on_entity(&mut stack, player, event.target) .await; } player.inventory().set_held_item(stack).await; } } } }} } 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_cross(translation::java::MULTIPLAYER_DISCONNECT_INVALID_ENTITY_ATTACKED, translation::java::MULTIPLAYER_DISCONNECT_INVALID_ENTITY_ATTACKED, [],)) .await; } } }} } } #[expect(clippy::too_many_lines)] pub async fn handle_player_action( &self, player: &Arc, player_action: SPlayerAction, server: &Server, ) { if !player.has_client_loaded() { return; } player.update_last_action_time(); match Status::try_from(player_action.status.0) { Ok(status) => match status { Status::StartedDigging => { if !player.can_interact_with_block_at(&player_action.position, 1.0) { warn!( "Player {0} tried to interact with block out of reach at {1}", player.gameprofile.name, player_action.position ); self.update_sequence(player, player_action.sequence.0); return; } let position = player_action.position; let entity = &player.get_entity(); let world = entity.world.load_full(); let (block, state) = world.get_block_and_state(&position); if let Some(server_arc) = world.server.upgrade() { let mut event = crate::plugin::api::events::block::block_damage::BlockDamageEvent::new( player.clone(), block, position, false, ); server_arc .plugin_manager .fire(&server_arc, &mut event) .await; if event.cancelled { self.update_sequence(player, player_action.sequence.0); return; } } if block == &pumpkin_data::Block::NOTE_BLOCK { let props = pumpkin_data::block_properties::NoteBlockLikeProperties::from_state_id( state.id, block, ); crate::block::blocks::note::NoteBlock::play_note(&props, &world, &position) .await; player .increment_stat( StatisticCategory::Custom, CustomStatistic::PlayNoteblock as i32, 1, ) .await; } let inventory = player.inventory(); let held = inventory.held_item().await; if !server.item_registry.can_mine(held.item, player) { self.enqueue_packet(&CBlockUpdate::new( position, VarInt(i32::from(state.id.as_u16())), )) .await; self.update_sequence(player, player_action.sequence.0); return; } // TODO: do validation // TODO: Config if player.gamemode.load() == GameMode::Creative { // Block break & play sound let new_state = world .break_block( &position, Some(player.clone()), BlockFlags::NOTIFY_NEIGHBORS | BlockFlags::SKIP_DROPS, ) .await; if new_state.is_some() { server .block_registry .broken(&world, block, player, &position, server, state) .await; } self.sync_block_state_to_client(&world, position).await; self.update_sequence(player, player_action.sequence.0); return; } player.start_mining_time.store( player.tick_counter.load(Ordering::Relaxed), Ordering::Relaxed, ); if !state.is_air() { let speed = block::calc_block_breaking(player, state, block).await; // Instant break if speed >= 1.0 { let broken_state = world.get_block_state(&position); let can_harvest = player.can_harvest(broken_state, block).await; let new_state = world .break_block( &position, Some(player.clone()), BlockFlags::NOTIFY_NEIGHBORS, ) .await; if new_state.is_some() { server .block_registry .broken(&world, block, player, &position, server, broken_state) .await; player.apply_tool_damage_for_block_break(broken_state).await; if can_harvest { player.add_exhaustion(MINE_BLOCK_EXHAUSTION).await; } let item_id = player.inventory().held_item().await.item.id; player .increment_stat(StatisticCategory::Used, item_id as i32, 1) .await; player .increment_stat( StatisticCategory::Mined, broken_state.id.as_u16() as i32, 1, ) .await; } self.sync_block_state_to_client(&world, position).await; } else { player.mining.store(true, Ordering::Relaxed); *player.mining_pos.lock().await = position; let progress = (speed * 10.0) as i32; player .current_block_breaking_speed .store(speed.to_bits(), Ordering::Relaxed); world .set_block_breaking( entity, position, BlockBreakingProgress::Start { stage: progress, speed, }, ) .await; player .current_block_destroy_stage .store(progress, Ordering::Relaxed); } } self.update_sequence(player, player_action.sequence.0); } Status::CancelledDigging => { if !player.can_interact_with_block_at(&player_action.position, 1.0) { warn!( "Player {0} tried to interact with block out of reach at {1}", player.gameprofile.name, player_action.position ); self.update_sequence(player, player_action.sequence.0); return; } player.mining.store(false, Ordering::Relaxed); let entity = &player.get_entity(); entity .world .load() .set_block_breaking( entity, player_action.position, BlockBreakingProgress::Stop, ) .await; self.update_sequence(player, player_action.sequence.0); } Status::FinishedDigging => { // TODO: do validation let location = player_action.position; if !player.can_interact_with_block_at(&location, 1.0) { warn!( "Player {0} tried to interact with block out of reach at {1}", player.gameprofile.name, player_action.position ); self.update_sequence(player, player_action.sequence.0); return; } // Block break & play sound let entity = &player.get_entity(); let world = entity.world.load_full(); player.mining.store(false, Ordering::Relaxed); world .set_block_breaking(entity, location, BlockBreakingProgress::Stop) .await; let (block, state) = world.get_block_and_state(&location); let block_drop = player.gamemode.load() != GameMode::Creative && player.can_harvest(state, block).await; let new_state = world .break_block( &location, Some(player.clone()), if block_drop { BlockFlags::NOTIFY_NEIGHBORS } else { BlockFlags::SKIP_DROPS | BlockFlags::NOTIFY_NEIGHBORS }, ) .await; if new_state.is_some() { server .block_registry .broken(&world, block, player, &location, server, state) .await; player.apply_tool_damage_for_block_break(state).await; if block_drop { player.add_exhaustion(MINE_BLOCK_EXHAUSTION).await; } let item_id = player.inventory().held_item().await.item.id; player .increment_stat(StatisticCategory::Used, item_id as i32, 1) .await; player .increment_stat(StatisticCategory::Mined, state.id.as_u16() as i32, 1) .await; } self.sync_block_state_to_client(&world, location).await; self.update_sequence(player, player_action.sequence.0); } Status::DropItem => { player.drop_held_item(false).await; } Status::DropItemStack => { player.drop_held_item(true).await; } Status::ReleaseItemInUse => { let item_in_use = player.living_entity.item_in_use.lock().await.clone(); if let Some(stack) = item_in_use { server.item_registry.on_stopped_using(&stack, player).await; } player.living_entity.clear_active_hand().await; } Status::SwapItem => { player.swap_item().await; } Status::SpearJab => { debug!("todo"); } }, Err(_) => self.kick(TextComponent::text("Invalid status")).await, } } pub async fn handle_keep_alive(&self, player: &Player, keep_alive: SKeepAlive) { if self.wait_for_keep_alive.load(Ordering::Relaxed) && keep_alive.keep_alive_id == self.keep_alive_id.load() { let ping = self.last_keep_alive_time.load().elapsed(); // Vanilla logic player.ping.store( (player.ping.load(Ordering::Relaxed) * 3 + ping.as_millis() as u32) / 4, Ordering::Relaxed, ); self.wait_for_keep_alive.store(false, Ordering::Relaxed); } else { self.kick(pumpkin_macros::translate_cross!( translation::java::DISCONNECT_TIMEOUT, translation::bedrock::DISCONNECT_TIMEOUT )) .await; } } pub fn update_sequence(&self, _player: &Player, sequence: i32) { if sequence < 0 { error!("Expected packet sequence >= 0"); } self.packet_sequence.store( self.packet_sequence.load(Ordering::Relaxed).max(sequence), Ordering::Relaxed, ); } async fn sync_block_state_to_client(&self, world: &World, position: BlockPos) { let synced_state_id = world.get_block_state_id(&position); self.send_packet_now(&CBlockUpdate::new( position, VarInt(i32::from(synced_state_id.as_u16())), )) .await; } pub async fn handle_player_abilities( &self, player: &Arc, player_abilities: SPlayerAbilities, server: &Arc, ) { let (flying, allow_flying) = { let abilities = player.abilities.lock().await; (abilities.flying, abilities.allow_flying) }; // Set the flying ability let new_flying = player_abilities.flags & 0x02 != 0 && allow_flying; if flying != new_flying { send_cancellable! {{ server; PlayerToggleFlightEvent::new(player.clone(), new_flying); 'after: { if event.is_flying { player.living_entity.fall_distance.store(0.0); } player.abilities.lock().await.flying = event.is_flying; } 'cancelled: { player.send_abilities_update().await; } }} } } pub async fn handle_play_ping_request(&self, request: SPlayPingRequest) { self.enqueue_packet(&CPingResponse::new(request.payload)) .await; } #[allow(clippy::too_many_lines)] pub async fn handle_use_item_on( &self, player: &Arc, use_item_on: SUseItemOn, server: &Arc, ) -> Result<(), BlockPlacingError> { if !player.has_client_loaded() { return Ok(()); } player.update_last_action_time(); self.update_sequence(player, use_item_on.sequence.0); let position = use_item_on.position; let cursor_pos = use_item_on.cursor_pos; let mut should_try_decrement = false; if !player.can_interact_with_block_at(&position, 1.0) { // TODO: maybe log? return Err(BlockPlacingError::BlockOutOfReach); } let Ok(face) = BlockDirection::try_from(use_item_on.face.0) else { return Err(BlockPlacingError::InvalidBlockFace); }; let Ok(hand) = Hand::try_from(use_item_on.hand.0) else { return Err(BlockPlacingError::InvalidHand); }; if player.gamemode.load() == GameMode::Spectator { // TODO: openMenu return Ok(()); } let inventory = player.inventory(); let held_item = inventory.held_item().await; let off_hand_item = inventory.off_hand_item().await; let held_item_empty = held_item.is_empty(); let off_hand_item_empty = off_hand_item.is_empty(); let item_id = if matches!(hand, Hand::Left) { held_item.item.id } else { off_hand_item.item.id }; player .increment_stat(StatisticCategory::Used, item_id as i32, 1) .await; let entity = &player.get_entity(); let world = entity.world.load_full(); let block = world.get_block(&position); let event = PlayerInteractEvent::new( player, InteractAction::RightClickBlock, block, Some(position), ); send_cancellable! {{ server; event; 'cancelled: { let state_id = world.get_block_state_id(&position); self.enqueue_packet(&CBlockUpdate::new( position, VarInt(i32::from(state_id.as_u16())), )) .await; return Ok(()); } }} let mut item = if matches!(hand, Hand::Left) { held_item } else { off_hand_item }; let equipment_slot = if matches!(hand, Hand::Left) { EquipmentSlot::MAIN_HAND } else { EquipmentSlot::OFF_HAND }; let sneaking = player.get_entity().is_sneaking(); // Code based on the java class ServerPlayerInteractionManager if !(sneaking && (!held_item_empty || !off_hand_item_empty)) { let result = self .call_use_item_on( player, &position, &cursor_pos, &face, &mut item, &equipment_slot, &world, block, server, ) .await; if result.consumes_action() { // TODO: Trigger ANY_BLOCK_USE Criteria if matches!(result, BlockActionResult::SuccessServer) { player.swing_hand(hand, true).await; } return Ok(()); } } let slot_index = if matches!(hand, Hand::Left) { inventory.get_selected_slot() as usize } else { PlayerInventory::OFF_HAND_SLOT }; if item.is_empty() { // TODO item cool down // If the hand is empty we stop here return Ok(()); } let before = item.clone(); server .item_registry .use_on_block(&mut item, player, position, face, cursor_pos, block, server) .await; // Check if the item is a block, because not every item can be placed :D let item_id = item.item.id; if let Some(block) = Block::from_item_id(item_id) { should_try_decrement = self .run_is_block_place(player, block, server, use_item_on, position, face) .await?; } if should_try_decrement { // TODO: Config // Decrease block count if player.gamemode.load() != GameMode::Creative { item.decrement(1); } } let after = item.clone(); // Broadcast the break entity status before the slot sync; the client // needs the old item texture in the slot for break particles. if !before.is_empty() && after.is_empty() { let slot = if slot_index == player.inventory.get_selected_slot() as usize { &EquipmentSlot::MAIN_HAND } else { &EquipmentSlot::OFF_HAND }; player.world().send_entity_status( player.get_entity(), equipment_break_status(slot), None, ); } if !after.are_equal(&before) { player.sync_hand_slot(slot_index, after.clone()).await; inventory.set_stack_in_hand(hand, after).await; } Ok(()) } #[expect(clippy::too_many_arguments)] async fn call_use_item_on( &self, player: &Arc, position: &BlockPos, cursor_pos: &Vector3, face: &BlockDirection, held_item: &mut ItemStack, equipment_slot: &EquipmentSlot, world: &Arc, block: &Block, server: &Arc, ) -> BlockActionResult { let result = server .block_registry .use_with_item( block, player, position, &BlockHitResult { face, cursor_pos }, held_item, equipment_slot, server, world, ) .await; if result.consumes_action() { // TODO: Trigger ITEM_USED_ON_BLOCK Criteria return result; } if matches!(result, BlockActionResult::PassToDefaultBlockAction) { let result = server .block_registry .on_use( block, player, position, &BlockHitResult { face, cursor_pos }, server, world, ) .await; if result.consumes_action() { // TODO: Trigger DEFAULT_BLOCK_USE Criteria return result; } } BlockActionResult::Pass } pub async fn handle_sign_update(&self, player: &Player, sign_data: SUpdateSign<'_>) { let world = player.get_entity().world.load_full(); let Some(block_entity) = world.get_block_entity(&sign_data.location) else { return; }; let Some(sign_entity) = block_entity.as_any().downcast_ref::() else { return; }; if sign_entity.is_waxed.load(Ordering::Relaxed) { return; } let text = if sign_data.is_front_text { &sign_entity.front_text } else { &sign_entity.back_text }; *text .messages .lock() .unwrap_or_else(std::sync::PoisonError::into_inner) = [ sign_data.line_1.into(), sign_data.line_2.into(), sign_data.line_3.into(), sign_data.line_4.into(), ]; *sign_entity.currently_editing_player.lock().await = None; world.update_block_entity(&block_entity); } pub async fn handle_edit_book(&self, player: &Player, packet: SEditBook<'_>) { let held_stack = player.inventory().held_item().await; if held_stack.item.id != Item::WRITABLE_BOOK.id { return; } let pages: Vec = packet.pages.iter().map(|p| (*p).to_string()).collect(); if let Some(title) = packet.title { let mut written_book = ItemStack::new(1, &Item::WRITTEN_BOOK); let content = WrittenBookContentImpl { title: title.to_string(), author: player.gameprofile.name.clone(), pages, }; written_book .patch .push((DataComponent::WrittenBookContent, Some(content.to_dyn()))); player.inventory().set_held_item(written_book).await; } else { let mut writable_book = held_stack; let content = WritableBookContentImpl { pages }; writable_book .patch .retain(|(component, _)| *component != DataComponent::WritableBookContent); writable_book .patch .push((DataComponent::WritableBookContent, Some(content.to_dyn()))); player.inventory().set_held_item(writable_book).await; } } pub async fn handle_use_item( &self, player: &Arc, use_item: &SUseItem, server: &Arc, ) { if !player.has_client_loaded() { return; } player.update_last_action_time(); let inventory = player.inventory(); let Ok(hand) = Hand::from_packet_id(use_item.hand.0) else { self.kick(TextComponent::text("InvalidHand")).await; return; }; self.update_sequence(player, use_item.sequence.0); let mut item_in_hand = inventory.get_stack_in_hand(hand).await; let (item_id, _item) = (item_in_hand.item.id, item_in_hand.item); player .increment_stat(StatisticCategory::Used, item_id as i32, 1) .await; let hit_result = player .world() .raycast( player.eye_position(), player.eye_position().add( &(Vector3::rotation_vector(f64::from(use_item.pitch), f64::from(use_item.yaw)) * 4.5), ), async |pos, world| { let block = world.get_block(pos); block != &Block::AIR && block != &Block::WATER && block != &Block::LAVA }, ) .await; let event = if let Some((hit_pos, _hit_dir)) = hit_result { PlayerInteractEvent::new( player, InteractAction::RightClickBlock, player.world().get_block(&hit_pos), Some(hit_pos), ) } else { PlayerInteractEvent::new(player, InteractAction::RightClickAir, &Block::AIR, None) }; let (item_for_use, stack_for_use) = (item_in_hand.item, item_in_hand.clone()); self.prepare_hand_item_for_use(player, hand, &mut item_in_hand) .await; if !self .should_continue_use_after_fish_event(server, player, hand, item_for_use) .await { return; } send_cancellable! {{ server; event; 'after: { server.item_registry.on_use(&stack_for_use, player).await; } }} } async fn prepare_hand_item_for_use( &self, player: &Arc, hand: Hand, held: &mut ItemStack, ) { let inventory = player.inventory(); if let Some(cooldown) = held.get_use_cooldown() { let group = cooldown .cooldown_group .clone() .unwrap_or_else(|| held.item.registry_key.to_string()); if player.is_on_cooldown(&group).await { return; } } if held.get_data_component::().is_some() || held.get_data_component::().is_some() { // If its food we want to make sure we can actually consume it if let Some(food) = held.get_data_component::() { if player.abilities.lock().await.invulnerable || food.can_always_eat || player.hunger_manager.level.load() < 20 { player .living_entity .set_active_hand(hand, held.clone(), held.get_max_use_time()) .await; } } else { player .living_entity .set_active_hand(hand, held.clone(), held.get_max_use_time()) .await; } } let equipment_slot = held .get_data_component::() .map(|equippable| equippable.slot.clone()); if let Some(slot) = equipment_slot { // The equipment lock has to be released before touching the hand again: // the off hand lives in the same map, so holding it here would deadlock. let current_equipped = inventory.entity_equipment.lock().await.get(&slot); if current_equipped.are_items_and_components_equal(held) { return; } player.enqueue_equipment_change(&slot, held).await; let equipped = if current_equipped.is_empty() { let equipped = held.clone(); held.decrement_unless_creative(player.gamemode.load(), 1); equipped } else { std::mem::replace(held, current_equipped) }; inventory.entity_equipment.lock().await.put(&slot, equipped); inventory.set_stack_in_hand(hand, held.clone()).await; } } async fn should_continue_use_after_fish_event( &self, server: &Arc, player: &Arc, hand: Hand, item_for_use: &Item, ) -> bool { if item_for_use.id != Item::FISHING_ROD.id { return true; } // TODO: Apply fishing rod durability on retrieval based on catch type. let mut fish_event = PlayerFishEvent::new( player.clone(), None, uuid::Uuid::nil(), String::new(), PlayerFishState::Fishing, hand, 0, ); server.plugin_manager.fire(server, &mut fish_event).await; !fish_event.cancelled } pub async fn handle_set_held_item( &self, server: &Arc, player: &Player, held: SSetHeldItem, ) { player.update_last_action_time(); let slot = held.slot; if !(0..=8).contains(&slot) { self.kick(TextComponent::text("Invalid held slot")).await; return; } let slot = slot as u8; let previous_slot = player.inventory.get_selected_slot(); let Some(player_arc) = player.world().get_player_by_uuid(player.gameprofile.id) else { return; }; let mut event = PlayerItemHeldEvent::new(player_arc, previous_slot, slot); server.plugin_manager.fire(server, &mut event).await; if event.cancelled { player .client .enqueue_packet(&CSetSelectedSlot::new(previous_slot as i8)) .await; return; } let inv = player.inventory(); inv.set_selected_slot(slot); let stack = inv.held_item().await; let equipment = &[(EquipmentSlot::MAIN_HAND, stack)]; player.living_entity.send_equipment_changes(equipment); } pub async fn handle_set_creative_slot( &self, player: &Player, packet: SSetCreativeSlot, ) -> Result<(), InventoryError> { if player.gamemode.load() != GameMode::Creative { return Err(InventoryError::PermissionError); } let is_negative = packet.slot < 0; let valid_slot = packet.slot >= 1 && packet.slot as usize <= 45; let item_stack = packet .clicked_item .to_stack_for_version(&self.version.load()); let is_legal = item_stack.is_empty() || item_stack.item_count <= item_stack.get_max_stack_size(); if valid_slot && is_legal { let mut player_screen_handler = player.player_screen_handler.lock().await; let is_armor_equipped = player_screen_handler .get_slot(packet.slot as usize) .get_stack() .await .are_equal(&item_stack); if !is_armor_equipped { if (5..9).contains(&packet.slot) { player .enqueue_equipment_change( &match packet.slot { 5 => EquipmentSlot::HEAD, 6 => EquipmentSlot::CHEST, 7 => EquipmentSlot::LEGS, 8 => EquipmentSlot::FEET, _ => { tracing::error!("Invalid armor slot: {}", packet.slot); EquipmentSlot::HEAD } }, &item_stack, ) .await; } else if (36..45).contains(&packet.slot) { let slot = packet.slot - 36; if player.inventory().get_selected_slot() == slot as u8 { let equipment = &[(EquipmentSlot::MAIN_HAND, item_stack.clone())]; player.living_entity.send_equipment_changes(equipment); } } } player_screen_handler .get_slot(packet.slot as usize) .set_stack(item_stack.clone()) .await; player_screen_handler.set_received_stack(packet.slot as usize, item_stack); player_screen_handler.send_content_updates().await; drop(player_screen_handler); } else if is_negative && is_legal { // Item drop player.drop_item(item_stack).await; } Ok(()) } pub async fn handle_chunk_batch(&self, player: &Player, packet: SChunkBatch) { player .chunk_manager .lock() .await .handle_acknowledge(packet.chunks_per_tick); trace!( "Client requested {} chunks per tick", packet.chunks_per_tick ); } pub async fn handle_close_container( &self, player: &Arc, _server: &Server, _packet: SCloseContainer, ) { player.on_handled_screen_closed().await; } pub async fn handle_command_suggestion( &self, player: &Arc, packet: SCommandSuggestion<'_>, server: &Arc, ) { let Some(cmd) = &packet.command.get(1..) else { return; }; let Some((last_word_start, _)) = cmd.char_indices().rfind(|(_, c)| c.is_whitespace()) else { return; }; let suggestions = server .command_dispatcher .read() .await .suggest(cmd, &player.get_command_source(server).await) .await; let response = CCommandSuggestions::new( packet.id, ((last_word_start + 2) as i32).into(), ((cmd.len() - last_word_start - 1) as i32).into(), suggestions.into(), ); self.enqueue_packet(&response).await; } pub fn handle_cookie_response(&self, packet: &SPCookieResponse<'_>) { // TODO: allow plugins to access this debug!( "Received cookie_response[play]: key: \"{}\", payload_length: \"{:?}\"", packet.key, packet.payload.as_ref().map(|p| p.len()) ); } async fn run_is_block_place( &self, player: &Arc, block: &'static Block, server: &Arc, use_item_on: SUseItemOn, location: BlockPos, face: BlockDirection, ) -> Result { match server .block_registry .place_block(player, block, server, &use_item_on, location, face) .await { Ok(Some((final_block_pos, new_state))) => { self.send_packet_now(&CBlockUpdate::new( final_block_pos, VarInt(i32::from(new_state.as_u16())), )) .await; Ok(true) } Ok(None) => Ok(false), Err(crate::block::registry::BlockPlacingError::InvalidGamemode) => { Err(BlockPlacingError::InvalidGamemode) } Err(crate::block::registry::BlockPlacingError::BlockOutOfWorld) => { Err(BlockPlacingError::BlockOutOfWorld) } } } /// Checks if the block placed was a sign, then opens a dialog. pub async fn send_sign_packet(&self, block_position: BlockPos, is_front_text: bool) { self.enqueue_packet(&COpenSignEditor::new(block_position, is_front_text)) .await; } pub async fn handle_select_trade(&self, player: &Arc, packet: SSelectTrade) { let screen_handler = player.current_screen_handler.lock().await; let mut screen_handler = screen_handler.lock().await; if let Some(merchant) = screen_handler .as_any_mut() .downcast_mut::() { merchant .set_selected_offer(packet.selected_slot.0 as usize) .await; } } pub async fn handle_seen_advancement(&self, player: &Arc, packet: SSeenAdvancement) { if let SSeenAdvancement::OpenTab(tab) = packet { let advancement = Advancement::from_minecraft_name(&tab.to_string()); if advancement.is_some() { player .advancements .lock() .await .set_selected_tab(advancement) .await; } } } pub async fn handle_bundle_item_selected( &self, player: &Arc, packet: SBundleItemSelected, ) { if !player.has_client_loaded() { return; } player.update_last_action_time(); let selected_item_index = packet.selected_item_index.0; if selected_item_index < 0 && selected_item_index != -1 { self.kick(TextComponent::text("Invalid selected item index")) .await; return; } debug!( "Bundle item selected: Slot ID {}, Selected Item Index {}", packet.slot_id.0, selected_item_index ); } pub async fn handle_teleport_to_entity( &self, player: &Arc, packet: STeleportToEntity, server: &Server, ) { if !player.has_client_loaded() { return; } player.update_last_action_time(); if player.gamemode.load() != GameMode::Spectator { return; } if let Some(target_player) = server.get_player_by_uuid(packet.target) { let target_pos = target_player.living_entity.entity.pos.load(); let target_yaw = target_player.living_entity.entity.yaw.load(); let target_pitch = target_player.living_entity.entity.pitch.load(); let target_id = target_player.living_entity.entity.entity_id; player.camera_target_id.store(Some(target_id)); player .client .send_packet_now(&CSetCamera::new(target_id.into())) .await; player .request_teleport(target_pos, target_yaw, target_pitch) .await; } } pub fn handle_test_instance_block_action( &self, player: &Arc, packet: &STestInstanceBlockAction<'_>, ) { if !player.has_client_loaded() { return; } player.update_last_action_time(); debug!( "Test instance block action at {:?}: action={:?}", packet.pos, packet.action ); } pub fn handle_set_test_block(&self, player: &Arc, packet: &SSetTestBlock<'_>) { if !player.has_client_loaded() { return; } player.update_last_action_time(); debug!( "Set test block at {:?}: mode={:?}, message={}", packet.position, packet.mode, packet.message ); } pub fn handle_debug_subscription_request( &self, player: &Arc, packet: &pumpkin_protocol::java::server::play::SDebugSubscriptionRequest, ) { if player.permission_lvl.load() >= PermissionLvl::Two && packet.sample_type.0 == 0 { player .subscribed_debug_sample .store(true, Ordering::Relaxed); } } pub fn handle_debug_sample_subscription( &self, player: &Arc, packet: &pumpkin_protocol::java::server::play::SDebugSampleSubscription, ) { if player.permission_lvl.load() >= PermissionLvl::Two && packet.sample_type.0 == 0 { player .subscribed_debug_sample .store(true, Ordering::Relaxed); } } }