chore: implement all missing raknet packets

This commit is contained in:
Alexander Medvedev 2026-07-05 20:26:32 +02:00
parent 560a0ed77d
commit 4ec2315aef
18 changed files with 773 additions and 427 deletions

View File

@ -12,7 +12,7 @@ use pumpkin_protocol::bedrock::server::{
raknet::{ raknet::{
connection::SConnectionRequest, connection::SConnectionRequest,
open_connection::{SOpenConnectionRequest1, SOpenConnectionRequest2}, open_connection::{SOpenConnectionRequest1, SOpenConnectionRequest2},
unconnected_ping::SUnconnectedPing, unconnected_ping::{SUnconnectedPing, SUnconnectedPingOpenConnections},
}, },
request_chunk_radius::SRequestChunkRadius, request_chunk_radius::SRequestChunkRadius,
request_network_settings::SRequestNetworkSettings, request_network_settings::SRequestNetworkSettings,
@ -60,6 +60,8 @@ fn fuzz_serverbound_packets(payload: &[u8]) {
let _ = SOpenConnectionRequest2::read(&mut cursor); let _ = SOpenConnectionRequest2::read(&mut cursor);
cursor.set_position(0); cursor.set_position(0);
let _ = SUnconnectedPing::read(&mut cursor); let _ = SUnconnectedPing::read(&mut cursor);
cursor.set_position(0);
let _ = SUnconnectedPingOpenConnections::read(&mut cursor);
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View File

@ -0,0 +1,25 @@
use pumpkin_macros::packet;
use pumpkin_util::math::vector3::Vector3;
use crate::{codec::var_int::VarInt, serial::PacketWrite};
#[derive(PacketWrite)]
#[packet(61)]
pub struct CChangeDimension {
pub dimension: VarInt,
pub position: Vector3<f32>,
pub respawn: bool,
pub has_loading_screen_id: bool,
}
impl CChangeDimension {
#[must_use]
pub const fn new(dimension: i32, position: Vector3<f32>, respawn: bool) -> Self {
Self {
dimension: VarInt(dimension),
position,
respawn,
has_loading_screen_id: false,
}
}
}

View File

@ -3,6 +3,7 @@ pub mod add_item_actor;
pub mod add_player; pub mod add_player;
pub mod available_commands; pub mod available_commands;
pub mod boss_event; pub mod boss_event;
pub mod change_dimension;
pub mod chunk_radius_update; pub mod chunk_radius_update;
pub mod common; pub mod common;
pub mod container_open; pub mod container_open;
@ -50,6 +51,7 @@ pub use add_item_actor::*;
pub use add_player::*; pub use add_player::*;
pub use available_commands::*; pub use available_commands::*;
pub use boss_event::*; pub use boss_event::*;
pub use change_dimension::*;
pub use chunk_radius_update::*; pub use chunk_radius_update::*;
pub use common::*; pub use common::*;
pub use container_open::*; pub use container_open::*;

View File

@ -2,7 +2,7 @@ use std::net::SocketAddr;
use pumpkin_macros::packet; use pumpkin_macros::packet;
use crate::serial::PacketWrite; use crate::{bedrock::RAKNET_MAGIC, serial::PacketWrite};
#[derive(PacketWrite)] #[derive(PacketWrite)]
#[packet(0x03)] #[packet(0x03)]
pub struct CConnectedPong { pub struct CConnectedPong {
@ -46,3 +46,75 @@ impl CConnectionRequestAccepted {
} }
} }
} }
#[derive(PacketWrite)]
#[packet(0x12)]
pub struct CAlreadyConnected {
magic: [u8; 16],
server_guid: u64,
}
impl CAlreadyConnected {
#[must_use]
pub const fn new(server_guid: u64) -> Self {
Self {
magic: RAKNET_MAGIC,
server_guid,
}
}
}
#[derive(PacketWrite)]
#[packet(0x14)]
pub struct CNoFreeIncomingConnections {
magic: [u8; 16],
server_guid: u64,
}
impl CNoFreeIncomingConnections {
#[must_use]
pub const fn new(server_guid: u64) -> Self {
Self {
magic: RAKNET_MAGIC,
server_guid,
}
}
}
#[derive(PacketWrite)]
#[packet(0x17)]
pub struct CConnectionBanned {
magic: [u8; 16],
server_guid: u64,
}
impl CConnectionBanned {
#[must_use]
pub const fn new(server_guid: u64) -> Self {
Self {
magic: RAKNET_MAGIC,
server_guid,
}
}
}
#[derive(PacketWrite)]
#[packet(0x1A)]
pub struct CIpRecentlyConnected {
magic: [u8; 16],
server_guid: u64,
}
impl CIpRecentlyConnected {
#[must_use]
pub const fn new(server_guid: u64) -> Self {
Self {
magic: RAKNET_MAGIC,
server_guid,
}
}
}
#[derive(PacketWrite)]
#[packet(0x15)]
pub struct CDisconnect;

View File

@ -44,6 +44,7 @@ impl PacketRead for NetworkItemDescriptor {
let has_net_id = bool::read(buf)?; let has_net_id = bool::read(buf)?;
if has_net_id { if has_net_id {
let _variant = VarUInt::read(buf)?;
let _net_id = VarInt::read(buf)?; let _net_id = VarInt::read(buf)?;
} }

View File

@ -226,7 +226,7 @@ mod tests {
const PAYLOAD_LEN: usize = 2 * 1024 * 1024 + 1; const PAYLOAD_LEN: usize = 2 * 1024 * 1024 + 1;
let payload = vec![0x2a; PAYLOAD_LEN]; let payload = vec![0x2a; PAYLOAD_LEN];
let mut wire_buf = Vec::new(); let mut wire_buf = Vec::new();
let mut network_encoder = UDPNetworkEncoder::new(); let network_encoder = UDPNetworkEncoder::new();
network_encoder network_encoder
.write_game_packet( .write_game_packet(
0x01, 0x01,

View File

@ -119,8 +119,12 @@ impl UDPNetworkEncoder {
Ok(()) Ok(())
} }
pub const fn encryptor_mut(&mut self) -> Option<&mut BedrockEncryptor> {
self.encryptor.as_mut()
}
pub fn write_game_packet( pub fn write_game_packet(
&mut self, &self,
packet_id: u16, packet_id: u16,
sub_client_sender: SubClient, sub_client_sender: SubClient,
sub_client_target: SubClient, sub_client_target: SubClient,
@ -151,26 +155,22 @@ impl UDPNetworkEncoder {
.write_u8(0xfe) .write_u8(0xfe)
.map_err(|e| Error::other(e.to_string()))?; // Bedrock Game Packet Header .map_err(|e| Error::other(e.to_string()))?; // Bedrock Game Packet Header
let mut data_to_encrypt = Vec::new(); let mut data_to_write = Vec::new();
if let Some((_threshold, level)) = self.compression { if let Some((_threshold, level)) = self.compression {
// Write Compression Method (0x00 for Zlib) // Write Compression Method (0x00 for Zlib)
data_to_encrypt.push(0x00); data_to_write.push(0x00);
let mut encoder = DeflateEncoder::new(Vec::new(), Compression::new(level)); let mut encoder = DeflateEncoder::new(Vec::new(), Compression::new(level));
encoder.write_all(&inner_buffer)?; encoder.write_all(&inner_buffer)?;
let compressed_data = encoder.finish()?; let compressed_data = encoder.finish()?;
data_to_encrypt.extend_from_slice(&compressed_data); data_to_write.extend_from_slice(&compressed_data);
} else { } else {
data_to_encrypt.extend_from_slice(&inner_buffer); data_to_write.extend_from_slice(&inner_buffer);
} }
if let Some(encryptor) = &mut self.encryptor { writer.write_all(&data_to_write)?;
encryptor.encrypt(&mut data_to_encrypt);
}
writer.write_all(&data_to_encrypt)?;
Ok(()) Ok(())
} }

View File

@ -27,6 +27,8 @@ pub struct SPlayerAuthInput {
pub tick: VarULong, pub tick: VarULong,
pub delta: Vector3<f32>, pub delta: Vector3<f32>,
pub block_actions: Option<Vec<PlayerBlockAction>>, pub block_actions: Option<Vec<PlayerBlockAction>>,
pub item_interaction: Option<PlayerInventoryAction>,
pub item_stack_request: Option<crate::bedrock::server::item_stack_request::ItemStackRequest>,
pub vehicle_rotation: Option<Vector2<f32>>, pub vehicle_rotation: Option<Vector2<f32>>,
pub vehicle_unique_id: Option<VarLong>, pub vehicle_unique_id: Option<VarLong>,
pub analog_move: Vector2<f32>, pub analog_move: Vector2<f32>,
@ -52,22 +54,18 @@ impl PacketRead for SPlayerAuthInput {
let delta = Vector3::<f32>::read(reader)?; let delta = Vector3::<f32>::read(reader)?;
// 1. Perform Item Interaction // 1. Perform Item Interaction
if input_data.get(InputData::PerformItemInteraction as usize) { let item_interaction = if input_data.get(InputData::PerformItemInteraction as usize) {
// protocol.UseItemTransactionData (Simplified skip) Some(PlayerInventoryAction::read(reader)?)
let _action_type = VarUInt::read(reader)?; } else {
let action_count = VarUInt::read(reader)?.0; None
for _ in 0..action_count { };
let _type = VarUInt::read(reader)?;
let _pos = BlockPos::read(reader)?;
let _face = VarInt::read(reader)?;
}
}
// 2. Item Stack Request // 2. Item Stack Request
if input_data.get(InputData::PerformItemStackRequest as usize) { let item_stack_request = if input_data.get(InputData::PerformItemStackRequest as usize) {
// protocol.Single ItemStackRequest Some(crate::bedrock::server::item_stack_request::ItemStackRequest::read(reader)?)
return Err(Error::other("ItemStackRequest decoding not implemented")); } else {
} None
};
// 3. Block Actions // 3. Block Actions
let block_actions = if input_data.get(InputData::PerformBlockActions as usize) { let block_actions = if input_data.get(InputData::PerformBlockActions as usize) {
@ -109,6 +107,8 @@ impl PacketRead for SPlayerAuthInput {
tick, tick,
delta, delta,
block_actions, block_actions,
item_interaction,
item_stack_request,
vehicle_rotation, vehicle_rotation,
vehicle_unique_id, vehicle_unique_id,
analog_move, analog_move,
@ -118,6 +118,43 @@ impl PacketRead for SPlayerAuthInput {
} }
} }
#[derive(Debug)]
pub struct PlayerInventoryAction {
pub legacy_request_id: VarInt,
pub legacy_slots: Vec<crate::bedrock::server::inventory_transaction::LegacySetItemSlot>,
pub actions: Vec<crate::bedrock::server::inventory_transaction::InventoryAction>,
pub transaction: crate::bedrock::server::inventory_transaction::UseItemTransactionData,
}
impl PacketRead for PlayerInventoryAction {
fn read<R: Read>(buf: &mut R) -> Result<Self, Error> {
let legacy_request_id = VarInt::read(buf)?;
let mut legacy_slots = Vec::new();
if legacy_request_id.0 < -1 && (legacy_request_id.0 & 1) == 0 {
let slots_len = VarUInt::read(buf)?.0;
for _ in 0..slots_len {
legacy_slots.push(
crate::bedrock::server::inventory_transaction::LegacySetItemSlot::read(buf)?,
);
}
}
let actions_len = VarUInt::read(buf)?.0;
let mut actions = Vec::with_capacity(actions_len as usize);
for _ in 0..actions_len {
actions
.push(crate::bedrock::server::inventory_transaction::InventoryAction::read(buf)?);
}
let transaction =
crate::bedrock::server::inventory_transaction::UseItemTransactionData::read(buf)?;
Ok(Self {
legacy_request_id,
legacy_slots,
actions,
transaction,
})
}
}
#[derive(Debug, PacketRead)] #[derive(Debug, PacketRead)]
pub struct PlayerBlockAction { pub struct PlayerBlockAction {
pub action: VarInt, pub action: VarInt,

View File

@ -35,3 +35,6 @@ pub struct SNewIncomingConnection {
#[packet(0x15)] #[packet(0x15)]
pub struct SDisconnect; pub struct SDisconnect;
#[packet(0x16)]
pub struct SConnectionLost;

View File

@ -12,3 +12,14 @@ pub struct SUnconnectedPing {
#[serial(big_endian)] #[serial(big_endian)]
pub client_guid: u64, pub client_guid: u64,
} }
#[derive(PacketRead)]
#[packet(0x02)]
/// Used to request Server information like MOTD when connection is open?
pub struct SUnconnectedPingOpenConnections {
#[serial(big_endian)]
pub time: u64,
pub magic: [u8; 16],
#[serial(big_endian)]
pub client_guid: u64,
}

View File

@ -385,7 +385,242 @@ pub struct BlockRegistry {
fluids: FxHashMap<u16, Arc<dyn FluidBehaviour>>, fluids: FxHashMap<u16, Arc<dyn FluidBehaviour>>,
} }
#[derive(Debug)]
pub enum BlockPlacingError {
InvalidGamemode,
BlockOutOfWorld,
}
impl BlockRegistry { impl BlockRegistry {
fn entity_blocks_block_placement(entity: &dyn EntityBase) -> bool {
let base_entity = entity.get_entity();
if base_entity.is_removed()
|| base_entity
.no_clip
.load(std::sync::atomic::Ordering::Relaxed)
|| entity.is_spectator()
{
return false;
}
if entity.get_living_entity().is_some() {
return true;
}
let entity_type = base_entity.entity_type;
let resource_name = entity_type.resource_name;
entity_type == &pumpkin_data::entity::EntityType::END_CRYSTAL
|| entity_type == &pumpkin_data::entity::EntityType::FALLING_BLOCK
|| entity_type == &pumpkin_data::entity::EntityType::TNT
|| resource_name.ends_with("_minecart")
|| resource_name.ends_with("_boat")
|| resource_name.ends_with("_raft")
}
fn has_blocking_entity_in_box(world: &World, placed_box: &BoundingBox) -> bool {
let players = world.players.load();
if players.iter().any(|player| {
Self::entity_blocks_block_placement(player.as_ref())
&& player
.get_entity()
.bounding_box
.load()
.intersects(placed_box)
}) {
return true;
}
world.entities.load().iter().any(|entity| {
Self::entity_blocks_block_placement(entity.as_ref())
&& entity
.get_entity()
.bounding_box
.load()
.intersects(placed_box)
})
}
#[expect(clippy::too_many_lines)]
pub async fn place_block(
&self,
player: &Arc<Player>,
placed_block: &'static Block,
server: &Server,
use_item_on: &SUseItemOn,
location: BlockPos,
face: BlockDirection,
) -> Result<Option<(BlockPos, BlockStateId)>, BlockPlacingError> {
let entity = &player.get_entity();
match player.gamemode.load() {
pumpkin_util::GameMode::Spectator | pumpkin_util::GameMode::Adventure => {
return Err(BlockPlacingError::InvalidGamemode);
}
_ => {}
}
let clicked_block_pos = BlockPos(location.0);
let world = entity.world.load_full();
if location.0.y + face.to_offset().y < world.get_bottom_y() {
return Err(BlockPlacingError::BlockOutOfWorld);
}
if location.0.y + face.to_offset().y > world.get_top_y() {
player
.send_system_message_raw(
&pumpkin_util::text::TextComponent::translate_cross(
pumpkin_data::translation::java::BUILD_TOOHIGH,
pumpkin_data::translation::bedrock::BUILD_TOOHIGH,
vec![pumpkin_util::text::TextComponent::text(
(world.get_top_y()).to_string(),
)],
)
.color_named(pumpkin_util::text::color::NamedColor::Red),
true,
)
.await;
return Err(BlockPlacingError::BlockOutOfWorld);
}
let (clicked_block, clicked_block_state) = world.get_block_and_state(&clicked_block_pos);
let replace_clicked_block = if clicked_block == placed_block {
self.can_update_at(
&world,
clicked_block,
clicked_block_state.id,
&clicked_block_pos,
face,
use_item_on,
player,
)
.then_some(BlockIsReplacing::Itself(clicked_block_state.id))
} else if clicked_block_state.replaceable() {
if clicked_block == &Block::WATER {
use pumpkin_data::block_properties::{BlockProperties, WaterLikeProperties};
let water_props =
WaterLikeProperties::from_state_id(clicked_block_state.id, clicked_block);
Some(BlockIsReplacing::Water(water_props.level))
} else {
Some(BlockIsReplacing::Other)
}
} else {
None
};
let (final_block_pos, final_face, replacing) =
if let Some(replacing) = replace_clicked_block {
(clicked_block_pos, face.opposite(), replacing)
} else {
let block_pos = BlockPos(location.0 + face.to_offset());
let (previous_block, previous_block_state) = world.get_block_and_state(&block_pos);
let replace_previous_block = if previous_block == placed_block {
self.can_update_at(
&world,
previous_block,
previous_block_state.id,
&block_pos,
face.opposite(),
use_item_on,
player,
)
.then_some(BlockIsReplacing::Itself(previous_block_state.id))
} else {
previous_block_state.replaceable().then(|| {
if previous_block == &Block::WATER {
use pumpkin_data::block_properties::{
BlockProperties, WaterLikeProperties,
};
let water_props = WaterLikeProperties::from_state_id(
previous_block_state.id,
previous_block,
);
BlockIsReplacing::Water(water_props.level)
} else {
BlockIsReplacing::None
}
})
};
match replace_previous_block {
Some(replacing) => (block_pos, face.opposite(), replacing),
None => {
return Ok(None);
}
}
};
if !self.can_place_at(
Some(server),
Some(&*world),
&*world,
Some(player),
placed_block,
placed_block.default_state,
&final_block_pos,
Some(final_face),
Some(use_item_on),
) {
return Ok(None);
}
let new_state = self
.on_place(
server,
&world,
player,
placed_block,
&final_block_pos,
final_face,
replacing,
use_item_on,
)
.await;
// Mirror vanilla obstruction checks: only entities that block building should prevent
// placement. (e.g. arrows/xp orbs/displays/markers should not)
let state = BlockState::from_id(new_state);
for shape in state.get_block_collision_shapes() {
let placed_box = shape.at_pos(final_block_pos);
if Self::has_blocking_entity_in_box(world.as_ref(), &placed_box) {
return Ok(None);
}
}
let event = crate::plugin::block::block_place::BlockPlaceEvent::new(
player.clone(),
placed_block,
clicked_block,
final_block_pos,
true,
);
let event = server
.plugin_manager
.fire::<crate::plugin::block::block_place::BlockPlaceEvent>(event)
.await;
if event.cancelled {
return Ok(None);
}
let _replaced_id = world
.set_block_state(&final_block_pos, new_state, BlockFlags::NOTIFY_ALL)
.await;
self.player_placed(
&world,
placed_block,
new_state,
&final_block_pos,
face,
player,
)
.await;
Ok(Some((final_block_pos, new_state)))
}
pub fn register<T: BlockBehaviour + BlockMetadata + 'static>(&mut self, block: T) { pub fn register<T: BlockBehaviour + BlockMetadata + 'static>(&mut self, block: T) {
let ids = T::ids(); let ids = T::ids();
let val = Arc::new(block); let val = Arc::new(block);

View File

@ -2496,21 +2496,42 @@ impl Player {
last_pos.y.round() as i32, last_pos.y.round() as i32,
last_pos.z.round() as i32, last_pos.z.round() as i32,
)); ));
self.client match self.client.as_ref() {
.send_packet_now(&CRespawn::new( ClientPlatform::Java(java) => {
PlayerSpawnData::new( java.send_packet_now(&CRespawn::new(
new_world.dimension.clone(), PlayerSpawnData::new(
biome::hash_seed(new_world.level.seed.0), // seed new_world.dimension.clone(),
self.gamemode.load() as u8, biome::hash_seed(new_world.level.seed.0), // seed
self.previous_gamemode.load().unwrap_or(self.gamemode.load()) as i8, self.gamemode.load() as u8,
false, self.previous_gamemode.load().unwrap_or(self.gamemode.load()) as i8,
false, false,
Some((death_dimension, death_location)), false,
VarInt(self.get_entity().portal_cooldown.load(Ordering::Relaxed) as i32), Some((death_dimension, death_location)),
new_world.sea_level.into(), VarInt(self.get_entity().portal_cooldown.load(Ordering::Relaxed) as i32),
), new_world.sea_level.into(),
CRespawn::KEEP_ALL_DATA, ),
)).await; CRespawn::KEEP_ALL_DATA,
)).await;
}
ClientPlatform::Bedrock(bedrock) => {
let bedrock_dimension = if new_world.dimension == Dimension::OVERWORLD {
0
} else if new_world.dimension == Dimension::THE_NETHER {
1
} else if new_world.dimension == Dimension::THE_END {
2
} else {
0
};
let pos_f32 = Vector3::new(position.x as f32, position.y as f32, position.z as f32);
let change_dim_packet = pumpkin_protocol::bedrock::client::CChangeDimension::new(
bedrock_dimension,
pos_f32,
false,
);
bedrock.enqueue_packet(&change_dim_packet).await;
}
}
self.send_permission_lvl_update(); self.send_permission_lvl_update();
@ -4931,7 +4952,6 @@ impl InventoryPlayer for Player {
} }
ClientPlatform::Bedrock(bedrock) => { ClientPlatform::Bedrock(bedrock) => {
use pumpkin_protocol::bedrock::{ use pumpkin_protocol::bedrock::{
client::inventory_content::CInventoryContent,
client::inventory_slot::CInventorySlot, client::inventory_slot::CInventorySlot,
network_item::{ network_item::{
ContainerName, FullContainerName, NetworkItemStackDescriptor, ContainerName, FullContainerName, NetworkItemStackDescriptor,
@ -4948,23 +4968,20 @@ impl InventoryPlayer for Player {
if window_id == 0 { if window_id == 0 {
tracing::info!( tracing::info!(
"enqueue_slot_packet: window_id is 0, sending CInventoryContent to Bedrock client" "enqueue_slot_packet: window_id is 0, sending CInventorySlot to Bedrock client"
); );
let mut slots = Vec::with_capacity(36); let slot_idx = packet.slot as usize;
let main_inventory = &self.inventory().main_inventory; let item_desc = NetworkItemStackDescriptor::from(&*packet.slot_data.0);
for s in main_inventory {
let stack = s.lock().await;
slots.push(NetworkItemStackDescriptor::from(&*stack));
}
let bedrock_packet = CInventoryContent { let bedrock_packet = CInventorySlot {
container_id: VarUInt(0), window_id: VarUInt(0),
slots, inventory_slot: VarUInt(slot_idx as u32),
full_container_name: FullContainerName { container_name: Some(FullContainerName {
container_name: ContainerName::Inventory, container_name: ContainerName::Inventory,
dynamic_id: None, dynamic_id: None,
}, }),
storage_item: NetworkItemStackDescriptor::default(), storage: None,
item: item_desc,
}; };
bedrock.enqueue_packet(&bedrock_packet).await; bedrock.enqueue_packet(&bedrock_packet).await;
} else { } else {
@ -5073,7 +5090,7 @@ impl InventoryPlayer for Player {
} }
ClientPlatform::Bedrock(bedrock) => { ClientPlatform::Bedrock(bedrock) => {
use pumpkin_protocol::bedrock::{ use pumpkin_protocol::bedrock::{
client::inventory_content::CInventoryContent, client::inventory_slot::CInventorySlot,
network_item::{ network_item::{
ContainerName, FullContainerName, NetworkItemStackDescriptor, ContainerName, FullContainerName, NetworkItemStackDescriptor,
}, },
@ -5081,24 +5098,21 @@ impl InventoryPlayer for Player {
use pumpkin_protocol::codec::var_uint::VarUInt; use pumpkin_protocol::codec::var_uint::VarUInt;
tracing::info!( tracing::info!(
"enqueue_slot_set_packet: slot={}, sending CInventoryContent to Bedrock client", "enqueue_slot_set_packet: slot={}, sending CInventorySlot to Bedrock client",
packet.slot.0 packet.slot.0
); );
let mut slots = Vec::with_capacity(36);
let main_inventory = &self.inventory().main_inventory;
for s in main_inventory {
let stack = s.lock().await;
slots.push(NetworkItemStackDescriptor::from(&*stack));
}
let bedrock_packet = CInventoryContent { let item_stack = &*packet.item.0;
container_id: VarUInt(0), let item_desc = NetworkItemStackDescriptor::from(item_stack);
slots, let bedrock_packet = CInventorySlot {
full_container_name: FullContainerName { window_id: VarUInt(0),
inventory_slot: VarUInt(packet.slot.0 as u32),
container_name: Some(FullContainerName {
container_name: ContainerName::Inventory, container_name: ContainerName::Inventory,
dynamic_id: None, dynamic_id: None,
}, }),
storage_item: NetworkItemStackDescriptor::default(), storage: None,
item: item_desc,
}; };
bedrock.enqueue_packet(&bedrock_packet).await; bedrock.enqueue_packet(&bedrock_packet).await;
} }

View File

@ -517,7 +517,7 @@ impl PumpkinServer {
}; };
let id = udp_buf[0]; let id = udp_buf[0];
let is_online = id & 128 != 0; let is_online = id & pumpkin_protocol::bedrock::RAKNET_VALID != 0;
if is_online { if is_online {
let be_clients = bedrock_clients.clone(); let be_clients = bedrock_clients.clone();

View File

@ -12,7 +12,6 @@ use pumpkin_protocol::bedrock::{
resource_pack_stack::CResourcePackStackPacket, resource_packs_info::CResourcePacksInfo, resource_pack_stack::CResourcePackStackPacket, resource_packs_info::CResourcePacksInfo,
start_game::Experiments, start_game::Experiments,
}, },
frame_set::FrameSet,
server::{login::SLogin, request_network_settings::SRequestNetworkSettings}, server::{login::SLogin, request_network_settings::SRequestNetworkSettings},
}; };
use pumpkin_protocol::bedrock::{ use pumpkin_protocol::bedrock::{
@ -256,9 +255,7 @@ impl BedrockClient {
return Ok(None); return Ok(None);
} }
let mut frame_set = FrameSet::default(); self.enqueue_packet_internal(&CPlayStatus::LoginSuccess)
self.write_game_packet_to_set(&CPlayStatus::LoginSuccess, &mut frame_set)
.await; .await;
let br_config = &server.advanced_config.resource_pack.bedrock; let br_config = &server.advanced_config.resource_pack.bedrock;
@ -289,10 +286,7 @@ impl BedrockClient {
world_template_version: String::new(), world_template_version: String::new(),
resource_packs: entries, resource_packs: entries,
}; };
self.write_game_packet_to_set(&packs_info, &mut frame_set) self.enqueue_packet_internal(&packs_info).await;
.await;
self.send_frame_set(frame_set, 0x84).await;
let new_config = PlayerConfig { let new_config = PlayerConfig {
locale: client_data.language_code.clone(), locale: client_data.language_code.clone(),
@ -327,10 +321,7 @@ impl BedrockClient {
} }
SResourcePackResponse::STATUS_HAVE_ALL_PACKS => { SResourcePackResponse::STATUS_HAVE_ALL_PACKS => {
debug!("Bedrock: SResourcePackResponse::STATUS_HAVE_ALL_PACKS"); debug!("Bedrock: SResourcePackResponse::STATUS_HAVE_ALL_PACKS");
let mut frame_set = FrameSet::default();
let br_config = &server.advanced_config.resource_pack.bedrock; let br_config = &server.advanced_config.resource_pack.bedrock;
// Convert your config packs into protocol stack entries // Convert your config packs into protocol stack entries
let resource_packs = if br_config.enabled { let resource_packs = if br_config.enabled {
br_config br_config
@ -346,22 +337,17 @@ impl BedrockClient {
Vec::new() Vec::new()
}; };
self.write_game_packet_to_set( self.enqueue_packet_internal(&CResourcePackStackPacket::new(
&CResourcePackStackPacket::new( br_config.force,
br_config.force, resource_packs,
resource_packs, CURRENT_BEDROCK_MC_VERSION.to_string(),
CURRENT_BEDROCK_MC_VERSION.to_string(), Experiments {
Experiments { names_size: 0,
names_size: 0, experiments_ever_toggled: false,
experiments_ever_toggled: false, },
}, false,
false, ))
),
&mut frame_set,
)
.await; .await;
self.send_frame_set(frame_set, 0x84).await;
} }
SResourcePackResponse::STATUS_COMPLETED => { SResourcePackResponse::STATUS_COMPLETED => {
debug!("Bedrock: SResourcePackResponse::STATUS_COMPLETED"); debug!("Bedrock: SResourcePackResponse::STATUS_COMPLETED");

View File

@ -18,14 +18,17 @@ use pumpkin_config::networking::compression::CompressionInfo;
use pumpkin_protocol::{ use pumpkin_protocol::{
BClientPacket, PacketDecodeError, RawPacket, BClientPacket, PacketDecodeError, RawPacket,
bedrock::{ bedrock::{
MTU, RAKNET_ACK, RAKNET_GAME_PACKET, RAKNET_NACK, RakReliability, SPLIT_FRAME_MAX_CONTENT, MTU, RAKNET_ACK, RAKNET_GAME_PACKET, RAKNET_NACK, RAKNET_VALID, RakReliability,
SubClient, UDP_HEADER_SIZE, SPLIT_FRAME_MAX_CONTENT, SubClient, UDP_HEADER_SIZE,
ack::Acknowledge, ack::Acknowledge,
client::{ client::{
disconnect_player::CDisconnectPlayer, disconnect_player::CDisconnectPlayer,
level_chunk::CLevelChunk, level_chunk::CLevelChunk,
play_status::CPlayStatus, play_status::CPlayStatus,
raknet::connection::CConnectionRequestAccepted, raknet::connection::{
CAlreadyConnected, CConnectionBanned, CConnectionRequestAccepted, CDisconnect,
CNoFreeIncomingConnections,
},
resource_packs_info::{CResourcePacksInfo, ResourcePackEntry}, resource_packs_info::{CResourcePacksInfo, ResourcePackEntry},
}, },
frame_set::{Frame, FrameSet}, frame_set::{Frame, FrameSet},
@ -48,10 +51,11 @@ use pumpkin_protocol::{
player_auth_input::SPlayerAuthInput, player_auth_input::SPlayerAuthInput,
raknet::{ raknet::{
connection::{ connection::{
SConnectedPing, SConnectionRequest, SDisconnect, SNewIncomingConnection, SConnectedPing, SConnectionLost, SConnectionRequest, SDisconnect,
SNewIncomingConnection,
}, },
open_connection::{SOpenConnectionRequest1, SOpenConnectionRequest2}, open_connection::{SOpenConnectionRequest1, SOpenConnectionRequest2},
unconnected_ping::SUnconnectedPing, unconnected_ping::{SUnconnectedPing, SUnconnectedPingOpenConnections},
}, },
request_ability::SRequestAbility, request_ability::SRequestAbility,
request_chunk_radius::SRequestChunkRadius, request_chunk_radius::SRequestChunkRadius,
@ -241,7 +245,7 @@ impl BedrockClient {
let mut interval = tokio::time::interval(std::time::Duration::from_millis(100)); let mut interval = tokio::time::interval(std::time::Duration::from_millis(100));
while !client.close_token.is_cancelled() { while !client.close_token.is_cancelled() {
let packet = tokio::select! { let mut packet = tokio::select! {
biased; biased;
() = client.close_token.cancelled() => break, () = client.close_token.cancelled() => break,
res = priority_packet_receiver.recv() => match res { res = priority_packet_receiver.recv() => match res {
@ -283,7 +287,7 @@ impl BedrockClient {
} }
if !resend.is_empty() { if !resend.is_empty() {
let encoder = client.network_writer.write().await; let encoder = client.network_writer.read().await;
for (seq, id, data) in resend { for (seq, id, data) in resend {
debug!("Resending reliable sequence {} (ID: {})", seq, id); debug!("Resending reliable sequence {} (ID: {})", seq, id);
if let Err(err) = encoder.write_packet(&data, client.address, &client.socket).await { if let Err(err) = encoder.write_packet(&data, client.address, &client.socket).await {
@ -299,6 +303,19 @@ impl BedrockClient {
}, },
}; };
// Encrypt the packet payload if encryption is enabled.
if packet.data.len() > 1 && packet.data[0] == 0xfe {
let mut encoder = client.network_writer.write().await;
if let Some(encryptor) = encoder.encryptor_mut() {
let mut data_to_encrypt = packet.data[1..].to_vec();
encryptor.encrypt(&mut data_to_encrypt);
let mut encrypted_payload = Vec::with_capacity(1 + data_to_encrypt.len());
encrypted_payload.push(0xfe);
encrypted_payload.extend_from_slice(&data_to_encrypt);
packet.data = encrypted_payload.into();
}
}
client client
.send_framed_packet_data(packet.data.to_vec(), RakReliability::ReliableOrdered) .send_framed_packet_data(packet.data.to_vec(), RakReliability::ReliableOrdered)
.await; .await;
@ -388,7 +405,7 @@ impl BedrockClient {
let mut packets_to_enqueue = Vec::with_capacity(encoded_payloads.len()); let mut packets_to_enqueue = Vec::with_capacity(encoded_payloads.len());
{ {
let mut encoder = self.network_writer.write().await; let encoder = self.network_writer.read().await;
for payload in encoded_payloads { for payload in encoded_payloads {
let mut packet_buf = Vec::new(); let mut packet_buf = Vec::new();
match encoder.write_game_packet( match encoder.write_game_packet(
@ -446,7 +463,7 @@ impl BedrockClient {
} }
{ {
let Ok(mut network_writer) = self.network_writer.try_write() else { let Ok(network_writer) = self.network_writer.try_read() else {
debug!("Failed to lock network writer for try_enqueue_packet"); debug!("Failed to lock network writer for try_enqueue_packet");
return; return;
}; };
@ -523,7 +540,7 @@ impl BedrockClient {
let mut packet_payload = Vec::new(); let mut packet_payload = Vec::new();
packet.write_packet(&mut packet_payload)?; packet.write_packet(&mut packet_payload)?;
let mut encoder = self.network_writer.write().await; let encoder = self.network_writer.read().await;
encoder.write_game_packet( encoder.write_game_packet(
P::PACKET_ID as u16, P::PACKET_ID as u16,
SubClient::Main, SubClient::Main,
@ -707,6 +724,10 @@ impl BedrockClient {
return; return;
} }
self.close_token.cancel(); self.close_token.cancel();
self.send_framed_packet(&CDisconnect, RakReliability::Unreliable)
.await;
self.be_clients.lock().await.remove(&self.address); self.be_clients.lock().await.remove(&self.address);
} }
@ -758,7 +779,7 @@ impl BedrockClient {
RAKNET_NACK => { RAKNET_NACK => {
self.handle_nack(&Acknowledge::read(reader)?).await; self.handle_nack(&Acknowledge::read(reader)?).await;
} }
0x80..=0x8d => { RAKNET_VALID..=0x8d => {
self.handle_frame_set(server, FrameSet::read(reader)?) self.handle_frame_set(server, FrameSet::read(reader)?)
.await?; .await?;
} }
@ -1020,8 +1041,7 @@ impl BedrockClient {
}; };
let pending = self.pending_profile.lock().await.take(); let pending = self.pending_profile.lock().await.take();
if let Some((profile, new_config)) = pending { if let Some((profile, new_config)) = pending {
let mut frame_set = FrameSet::default(); self.enqueue_packet_internal(&CPlayStatus::LoginSuccess)
self.write_game_packet_to_set(&CPlayStatus::LoginSuccess, &mut frame_set)
.await; .await;
let br_config = &server.advanced_config.resource_pack.bedrock; let br_config = &server.advanced_config.resource_pack.bedrock;
@ -1052,10 +1072,7 @@ impl BedrockClient {
world_template_version: String::new(), world_template_version: String::new(),
resource_packs: entries, resource_packs: entries,
}; };
self.write_game_packet_to_set(&packs_info, &mut frame_set) self.enqueue_packet_internal(&packs_info).await;
.await;
self.send_frame_set(frame_set, 0x84).await;
return PacketHandlerResult::ReadyToPlay(profile, new_config); return PacketHandlerResult::ReadyToPlay(profile, new_config);
} }
error!("Received ClientToServerHandshake but no pending profile was found."); error!("Received ClientToServerHandshake but no pending profile was found.");
@ -1226,7 +1243,7 @@ impl BedrockClient {
self.handle_connected_ping(SConnectedPing::read(reader)?) self.handle_connected_ping(SConnectedPing::read(reader)?)
.await; .await;
} }
SDisconnect::PACKET_ID => { SDisconnect::PACKET_ID | SConnectionLost::PACKET_ID => {
self.close().await; self.close().await;
} }
_ => { _ => {
@ -1236,6 +1253,7 @@ impl BedrockClient {
Ok(()) Ok(())
} }
#[expect(clippy::too_many_lines)]
pub async fn handle_offline_packet( pub async fn handle_offline_packet(
server: &Server, server: &Server,
packet_id: u8, packet_id: u8,
@ -1246,6 +1264,38 @@ impl BedrockClient {
) -> Result<(), Error> { ) -> Result<(), Error> {
let packet_id_i32 = i32::from(packet_id); let packet_id_i32 = i32::from(packet_id);
if packet_id_i32 == SOpenConnectionRequest1::PACKET_ID { if packet_id_i32 == SOpenConnectionRequest1::PACKET_ID {
let is_banned = {
let mut banned_ips = server.data.banned_ip_list.write().await;
banned_ips.get_entry(&addr.ip()).is_some()
};
if is_banned {
Self::send_offline_packet(
&CConnectionBanned::new(server.server_guid),
addr,
socket,
)
.await;
return Ok(());
}
let player_count = {
let status = server.get_status().lock().await;
status
.status_response
.players
.as_ref()
.map_or(0, |p| p.online) as u32
};
if player_count >= server.basic_config.max_players {
Self::send_offline_packet(
&CNoFreeIncomingConnections::new(server.server_guid),
addr,
socket,
)
.await;
return Ok(());
}
let old_client = { let old_client = {
let mut clients_guard = be_clients.lock().await; let mut clients_guard = be_clients.lock().await;
clients_guard.remove(&addr) clients_guard.remove(&addr)
@ -1269,6 +1319,20 @@ impl BedrockClient {
) )
.await; .await;
} }
SUnconnectedPingOpenConnections::PACKET_ID => {
let packet = SUnconnectedPingOpenConnections::read(payload)?;
Self::handle_unconnected_ping(
server,
SUnconnectedPing {
time: packet.time,
magic: packet.magic,
client_guid: packet.client_guid,
},
addr,
socket,
)
.await;
}
SOpenConnectionRequest1::PACKET_ID => { SOpenConnectionRequest1::PACKET_ID => {
Self::handle_open_connection_1( Self::handle_open_connection_1(
server, server,
@ -1279,6 +1343,20 @@ impl BedrockClient {
.await; .await;
} }
SOpenConnectionRequest2::PACKET_ID => { SOpenConnectionRequest2::PACKET_ID => {
let is_already_connected = {
let clients_guard = be_clients.lock().await;
clients_guard.contains_key(&addr)
};
if is_already_connected {
Self::send_offline_packet(
&CAlreadyConnected::new(server.server_guid),
addr,
socket,
)
.await;
return Ok(());
}
Self::handle_open_connection_2( Self::handle_open_connection_2(
server, server,
SOpenConnectionRequest2::read(payload)?, SOpenConnectionRequest2::read(payload)?,

View File

@ -57,21 +57,45 @@ use crate::{
use pumpkin_data::BlockDirection; use pumpkin_data::BlockDirection;
use tracing::{debug, info}; use tracing::{debug, info};
fn descriptor_to_stack(desc: &NetworkItemDescriptor) -> ItemStack { fn descriptor_to_stack(desc: &NetworkItemDescriptor, is_creative: bool) -> ItemStack {
if desc.id.0 == 0 || desc.stack_size == 0 { if desc.id.0 == 0 || desc.stack_size == 0 {
ItemStack::EMPTY.clone() ItemStack::EMPTY.clone()
} else if let Some(mapping) = pumpkin_data::item::JavaToBedrockItemMapping::from_bedrock(
desc.id.0 as i16,
desc.aux_value.0,
) {
ItemStack::new(desc.stack_size as u8, mapping.java_item)
} else { } else {
tracing::warn!( let mut mapped_item = None;
"Failed to map bedrock item id {} and data {} to Java item",
desc.id.0, if is_creative {
desc.aux_value.0 let index = (desc.id.0.saturating_sub(1)) as usize;
); if index < pumpkin_data::bedrock_creative::CREATIVE_ENTRIES.len() {
ItemStack::EMPTY.clone() let entry = pumpkin_data::bedrock_creative::CREATIVE_ENTRIES[index];
if let Some(mapping) = pumpkin_data::item::JavaToBedrockItemMapping::from_bedrock(
entry.item_id,
entry.item_aux_value,
) {
mapped_item = Some(mapping.java_item);
}
}
}
if mapped_item.is_none()
&& let Some(mapping) = pumpkin_data::item::JavaToBedrockItemMapping::from_bedrock(
desc.id.0 as i16,
desc.aux_value.0,
)
{
mapped_item = Some(mapping.java_item);
}
mapped_item.map_or_else(
|| {
tracing::warn!(
"Failed to map bedrock item id {} and data {} to Java item",
desc.id.0,
desc.aux_value.0
);
ItemStack::EMPTY.clone()
},
|item| ItemStack::new(desc.stack_size as u8, item),
)
} }
} }
@ -321,13 +345,15 @@ impl BedrockClient {
} }
if input_data.get(InputData::StartFlying as usize) { if input_data.get(InputData::StartFlying as usize) {
let mut abilities = player.abilities.lock().await; let flying = { player.abilities.lock().await.flying };
if !abilities.flying { if !flying {
send_cancellable! {{ send_cancellable! {{
server; server;
PlayerToggleFlightEvent::new(player.clone(), true); PlayerToggleFlightEvent::new(player.clone(), true);
'after: { 'after: {
abilities.flying = true; {
player.abilities.lock().await.flying = true;
};
player.send_abilities_update().await; player.send_abilities_update().await;
} }
'cancelled: { 'cancelled: {
@ -336,13 +362,15 @@ impl BedrockClient {
}} }}
} }
} else if input_data.get(InputData::StopFlying as usize) { } else if input_data.get(InputData::StopFlying as usize) {
let mut abilities = player.abilities.lock().await; let flying = { player.abilities.lock().await.flying };
if abilities.flying { if flying {
send_cancellable! {{ send_cancellable! {{
server; server;
PlayerToggleFlightEvent::new(player.clone(), false); PlayerToggleFlightEvent::new(player.clone(), false);
'after: { 'after: {
abilities.flying = false; {
player.abilities.lock().await.flying = false;
};
player.send_abilities_update().await; player.send_abilities_update().await;
} }
'cancelled: { 'cancelled: {
@ -504,12 +532,13 @@ impl BedrockClient {
player_screen_handler.send_content_updates().await; player_screen_handler.send_content_updates().await;
} }
let is_creative = player.gamemode.load() == GameMode::Creative;
for action in &packet.actions { for action in &packet.actions {
use pumpkin_protocol::bedrock::server::inventory_transaction::InventoryActionSource; use pumpkin_protocol::bedrock::server::inventory_transaction::InventoryActionSource;
let source_type = InventoryActionSource::from(action.source_type); let source_type = InventoryActionSource::from(action.source_type);
if source_type == InventoryActionSource::World { if source_type == InventoryActionSource::World {
let old_stack = descriptor_to_stack(&action.old_item); let old_stack = descriptor_to_stack(&action.old_item, is_creative);
let new_stack = descriptor_to_stack(&action.new_item); let new_stack = descriptor_to_stack(&action.new_item, is_creative);
if old_stack.is_empty() && !new_stack.is_empty() { if old_stack.is_empty() && !new_stack.is_empty() {
player.drop_item(new_stack).await; player.drop_item(new_stack).await;
} }
@ -517,7 +546,7 @@ impl BedrockClient {
if let Some(screen_slot) = if let Some(screen_slot) =
map_bedrock_slot_to_screen_handler(window_id, action.inventory_slot) map_bedrock_slot_to_screen_handler(window_id, action.inventory_slot)
{ {
let item_stack = descriptor_to_stack(&action.new_item); let item_stack = descriptor_to_stack(&action.new_item, is_creative);
let mut player_screen_handler = player.player_screen_handler.lock().await; let mut player_screen_handler = player.player_screen_handler.lock().await;
@ -610,7 +639,16 @@ impl BedrockClient {
if data.action_type.0 == 0 { if data.action_type.0 == 0 {
// Click block // Click block
let is_creative = player.gamemode.load() == GameMode::Creative;
let client_stack = descriptor_to_stack(&data.item_in_hand, is_creative);
let held_item = player.inventory.held_item(); let held_item = player.inventory.held_item();
if !client_stack.is_empty() {
let mut server_stack = held_item.lock().await;
if server_stack.is_empty() || server_stack.item.id != client_stack.item.id {
*server_stack = client_stack.clone();
}
}
let result = server let result = server
.block_registry .block_registry
@ -648,6 +686,53 @@ impl BedrockClient {
) )
.await; .await;
} }
let mut stack = held_item.lock().await;
if !stack.is_empty() {
server
.item_registry
.use_on_block(
&mut stack,
player,
data.block_position,
face,
data.click_position,
block,
&server,
)
.await;
let item_id = stack.item.id;
if let Some(placed_block) = pumpkin_data::Block::from_item_id(item_id) {
let dummy_use_item_on =
pumpkin_protocol::java::server::play::SUseItemOn {
hand: VarInt(0),
position: data.block_position,
face: VarInt(data.block_face),
cursor_pos: data.click_position,
inside_block: false,
is_against_world_border: false,
sequence: VarInt(0),
};
if let Ok(Some(_)) = server
.block_registry
.place_block(
player,
placed_block,
&server,
&dummy_use_item_on,
data.block_position,
face,
)
.await
{
if player.gamemode.load() != GameMode::Creative {
stack.decrement(1);
}
}
}
}
} }
} }
TransactionData::UseItemOnEntity(data) => { TransactionData::UseItemOnEntity(data) => {

View File

@ -12,7 +12,7 @@ use tracing::{Level, debug, error, info, trace, warn};
use crate::block::BlockHitResult; use crate::block::BlockHitResult;
use crate::block::registry::BlockActionResult; use crate::block::registry::BlockActionResult;
use crate::block::{self, BlockIsReplacing}; use crate::block::{self};
use crate::entity::EntityBase; use crate::entity::EntityBase;
use crate::entity::equipment_break_status; use crate::entity::equipment_break_status;
use crate::entity::player::statistics::{CustomStatistic, StatisticCategory}; use crate::entity::player::statistics::{CustomStatistic, StatisticCategory};
@ -21,7 +21,6 @@ use crate::error::PumpkinError;
use crate::log_at_level; use crate::log_at_level;
use crate::net::PlayerConfig; use crate::net::PlayerConfig;
use crate::net::java::JavaClient; use crate::net::java::JavaClient;
use crate::plugin::block::block_place::BlockPlaceEvent;
use crate::plugin::player::changed_main_hand::PlayerChangedMainHandEvent; use crate::plugin::player::changed_main_hand::PlayerChangedMainHandEvent;
use crate::plugin::player::fish::{PlayerFishEvent, PlayerFishState}; use crate::plugin::player::fish::{PlayerFishEvent, PlayerFishState};
use crate::plugin::player::item_held::PlayerItemHeldEvent; use crate::plugin::player::item_held::PlayerItemHeldEvent;
@ -40,17 +39,14 @@ use crate::block::entities::sign::SignBlockEntity;
use crate::plugin::player::player_toggle_sprint_event::PlayerToggleSprintEvent; use crate::plugin::player::player_toggle_sprint_event::PlayerToggleSprintEvent;
use crate::server::{Server, seasonal_events}; use crate::server::{Server, seasonal_events};
use crate::world::{World, chunker}; use crate::world::{World, chunker};
use pumpkin_data::block_properties::{ use pumpkin_data::block_properties::{BlockProperties, CommandBlockLikeProperties};
BlockProperties, CommandBlockLikeProperties, WaterLikeProperties,
};
use pumpkin_data::data_component_impl::{ use pumpkin_data::data_component_impl::{
BlocksAttacksImpl, ConsumableImpl, EquipmentSlot, EquippableImpl, FoodImpl, BlocksAttacksImpl, ConsumableImpl, EquipmentSlot, EquippableImpl, FoodImpl,
}; };
use pumpkin_data::entity::EntityType;
use pumpkin_data::item::Item; use pumpkin_data::item::Item;
use pumpkin_data::item_stack::ItemStack; use pumpkin_data::item_stack::ItemStack;
use pumpkin_data::sound::{Sound, SoundCategory}; use pumpkin_data::sound::{Sound, SoundCategory};
use pumpkin_data::{Advancement, Block, BlockDirection, BlockState, translation}; use pumpkin_data::{Advancement, Block, BlockDirection, translation};
use pumpkin_inventory::InventoryError; use pumpkin_inventory::InventoryError;
use pumpkin_inventory::merchant::merchant_screen_handler::MerchantScreenHandler; use pumpkin_inventory::merchant::merchant_screen_handler::MerchantScreenHandler;
use pumpkin_inventory::player::player_inventory::PlayerInventory; use pumpkin_inventory::player::player_inventory::PlayerInventory;
@ -77,10 +73,8 @@ use pumpkin_protocol::java::server::play::{
SSetJigsawBlock, SSetPlayerGround, SSetTestBlock, SSwingArm, STeleportToEntity, SSetJigsawBlock, SSetPlayerGround, SSetTestBlock, SSwingArm, STeleportToEntity,
STestInstanceBlockAction, SUpdateSign, SUseItem, SUseItemOn, Status, STestInstanceBlockAction, SUpdateSign, SUseItem, SUseItemOn, Status,
}; };
use pumpkin_util::math::boundingbox::BoundingBox;
use pumpkin_util::math::vector3::Vector3; use pumpkin_util::math::vector3::Vector3;
use pumpkin_util::math::{polynomial_rolling_hash, position::BlockPos, wrap_degrees}; use pumpkin_util::math::{polynomial_rolling_hash, position::BlockPos, wrap_degrees};
use pumpkin_util::text::color::NamedColor;
use pumpkin_util::{GameMode, text::TextComponent}; use pumpkin_util::{GameMode, text::TextComponent};
use pumpkin_world::generation::structure::structures::jigsaw::JigsawJointType; use pumpkin_world::generation::structure::structures::jigsaw::JigsawJointType;
use pumpkin_world::world::BlockFlags; use pumpkin_world::world::BlockFlags;
@ -2711,55 +2705,6 @@ impl JavaClient {
); );
} }
fn entity_blocks_block_placement(entity: &dyn EntityBase) -> bool {
let base_entity = entity.get_entity();
if base_entity.is_removed()
|| base_entity.no_clip.load(Ordering::Relaxed)
|| entity.is_spectator()
{
return false;
}
if entity.get_living_entity().is_some() {
return true;
}
// Matches vanilla's "blocksBuilding" intent for non-living entities:
// minecarts/boats/rafts + a few special entities.
let entity_type = base_entity.entity_type;
let resource_name = entity_type.resource_name;
entity_type == &EntityType::END_CRYSTAL
|| entity_type == &EntityType::FALLING_BLOCK
|| entity_type == &EntityType::TNT
|| resource_name.ends_with("_minecart")
|| resource_name.ends_with("_boat")
|| resource_name.ends_with("_raft")
}
fn has_blocking_entity_in_box(world: &World, placed_box: &BoundingBox) -> bool {
let players = world.players.load();
if players.iter().any(|player| {
Self::entity_blocks_block_placement(player.as_ref())
&& player
.get_entity()
.bounding_box
.load()
.intersects(placed_box)
}) {
return true;
}
world.entities.load().iter().any(|entity| {
Self::entity_blocks_block_placement(entity.as_ref())
&& entity
.get_entity()
.bounding_box
.load()
.intersects(placed_box)
})
}
#[expect(clippy::too_many_lines)]
async fn run_is_block_place( async fn run_is_block_place(
&self, &self,
player: &Arc<Player>, player: &Arc<Player>,
@ -2769,171 +2714,27 @@ impl JavaClient {
location: BlockPos, location: BlockPos,
face: BlockDirection, face: BlockDirection,
) -> Result<bool, BlockPlacingError> { ) -> Result<bool, BlockPlacingError> {
let entity = &player.get_entity(); match server
.block_registry
match player.gamemode.load() { .place_block(player, block, server, &use_item_on, location, face)
GameMode::Spectator | GameMode::Adventure => { .await
return Err(BlockPlacingError::InvalidGamemode); {
} Ok(Some((final_block_pos, new_state))) => {
_ => {} self.send_packet_now(&CBlockUpdate::new(
} final_block_pos,
VarInt(i32::from(new_state.as_u16())),
let clicked_block_pos = BlockPos(location.0); ))
let world = entity.world.load_full();
// Check if the block is under the world
if location.0.y + face.to_offset().y < world.get_bottom_y() {
return Err(BlockPlacingError::BlockOutOfWorld);
}
// Check the world's max build height
if location.0.y + face.to_offset().y > world.get_top_y() {
player
.send_system_message_raw(
&TextComponent::translate_cross(
translation::java::BUILD_TOOHIGH,
translation::bedrock::BUILD_TOOHIGH,
vec![TextComponent::text((world.get_top_y()).to_string())],
)
.color_named(NamedColor::Red),
true,
)
.await; .await;
return Err(BlockPlacingError::BlockOutOfWorld); Ok(true)
}
let (clicked_block, clicked_block_state) = world.get_block_and_state(&clicked_block_pos);
let replace_clicked_block = if clicked_block == block {
world
.block_registry
.can_update_at(
&world,
clicked_block,
clicked_block_state.id,
&clicked_block_pos,
face,
&use_item_on,
player,
)
.then_some(BlockIsReplacing::Itself(clicked_block_state.id))
} else if clicked_block_state.replaceable() {
if clicked_block == &Block::WATER {
let water_props =
WaterLikeProperties::from_state_id(clicked_block_state.id, clicked_block);
Some(BlockIsReplacing::Water(water_props.level))
} else {
Some(BlockIsReplacing::Other)
} }
} else { Ok(None) => Ok(false),
None Err(crate::block::registry::BlockPlacingError::InvalidGamemode) => {
}; Err(BlockPlacingError::InvalidGamemode)
}
let (final_block_pos, final_face, replacing) = Err(crate::block::registry::BlockPlacingError::BlockOutOfWorld) => {
if let Some(replacing) = replace_clicked_block { Err(BlockPlacingError::BlockOutOfWorld)
(clicked_block_pos, face.opposite(), replacing)
} else {
let block_pos = BlockPos(location.0 + face.to_offset());
let (previous_block, previous_block_state) = world.get_block_and_state(&block_pos);
let replace_previous_block = if previous_block == block {
world
.block_registry
.can_update_at(
&world,
previous_block,
previous_block_state.id,
&block_pos,
face.opposite(),
&use_item_on,
player,
)
.then_some(BlockIsReplacing::Itself(previous_block_state.id))
} else {
previous_block_state.replaceable().then(|| {
if previous_block == &Block::WATER {
let water_props = WaterLikeProperties::from_state_id(
previous_block_state.id,
previous_block,
);
BlockIsReplacing::Water(water_props.level)
} else {
BlockIsReplacing::None
}
})
};
match replace_previous_block {
Some(replacing) => (block_pos, face.opposite(), replacing),
None => {
// Don't place and don't decrement if the previous block is not replaceable
return Ok(false);
}
}
};
if !server.block_registry.can_place_at(
Some(server),
Some(&*world),
&*world,
Some(player),
block,
block.default_state,
&final_block_pos,
Some(final_face),
Some(&use_item_on),
) {
return Ok(false);
}
let new_state = server
.block_registry
.on_place(
server,
&world,
player,
block,
&final_block_pos,
final_face,
replacing,
&use_item_on,
)
.await;
// Mirror vanilla obstruction checks: only entities that block building should prevent
// placement. (e.g. arrows/xp orbs/displays/markers should not)
let state = BlockState::from_id(new_state);
for shape in state.get_block_collision_shapes() {
let placed_box = shape.at_pos(final_block_pos);
if Self::has_blocking_entity_in_box(world.as_ref(), &placed_box) {
return Ok(false);
} }
} }
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;
self.send_packet_now(&CBlockUpdate::new(
final_block_pos,
VarInt(i32::from(new_state.as_u16())),
))
.await;
server
.block_registry
.player_placed(&world, block, new_state, &final_block_pos, face, player)
.await;
// The block was placed successfully, so decrement their inventory
Ok(true)
} }
/// Checks if the block placed was a sign, then opens a dialog. /// Checks if the block placed was a sign, then opens a dialog.

View File

@ -79,7 +79,6 @@ use pumpkin_inventory::screen_handler::InventoryPlayer;
use pumpkin_nbt::{compound::NbtCompound, to_bytes_unnamed}; use pumpkin_nbt::{compound::NbtCompound, to_bytes_unnamed};
use pumpkin_protocol::bedrock::client::set_actor_data::{CSetActorData, PropertySyncData}; use pumpkin_protocol::bedrock::client::set_actor_data::{CSetActorData, PropertySyncData};
use pumpkin_protocol::bedrock::client::start_game::{CStartGame, ServerTelemetryData}; use pumpkin_protocol::bedrock::client::start_game::{CStartGame, ServerTelemetryData};
use pumpkin_protocol::bedrock::frame_set::FrameSet;
use pumpkin_protocol::java::client::play::{ use pumpkin_protocol::java::client::play::{
CBlockUpdate, CChunkBatchEnd, CChunkBatchStart, CChunkData, CDisguisedChatMessage, CExplosion, CBlockUpdate, CChunkBatchEnd, CChunkBatchStart, CChunkData, CDisguisedChatMessage, CExplosion,
CRespawn, CSetBlockDestroyStage, CWorldEvent, PlayerSpawnData, CRespawn, CSetBlockDestroyStage, CWorldEvent, PlayerSpawnData,
@ -2222,79 +2221,74 @@ impl World {
.await; .await;
}; };
let mut frame_set = FrameSet::default();
client client
.write_game_packet_to_set( .enqueue_packet_internal(&CUpdateAttributes {
&CUpdateAttributes { runtime_id: VarULong(runtime_id),
runtime_id: VarULong(runtime_id), attributes: vec![
attributes: vec![ Attribute {
Attribute { min_value: 0.0,
min_value: 0.0, max_value: 3.402_823_5E38,
max_value: 3.402_823_5E38, current_value: 0.1,
current_value: 0.1, default_min_value: 0.0,
default_min_value: 0.0, default_max_value: 3.402_823_5E38,
default_max_value: 3.402_823_5E38, default_value: 0.1,
default_value: 0.1, name: "minecraft:movement".to_string(),
name: "minecraft:movement".to_string(), modifiers_list_size: VarUInt(0),
modifiers_list_size: VarUInt(0), },
}, Attribute {
Attribute { min_value: 0.0,
min_value: 0.0, max_value: 3.402_823_5E38,
max_value: 3.402_823_5E38, current_value: 0.02,
current_value: 0.02, default_min_value: 0.0,
default_min_value: 0.0, default_max_value: 3.402_823_5E38,
default_max_value: 3.402_823_5E38, default_value: 0.02,
default_value: 0.02, name: "minecraft:underwater_movement".to_string(),
name: "minecraft:underwater_movement".to_string(), modifiers_list_size: VarUInt(0),
modifiers_list_size: VarUInt(0), },
}, Attribute {
Attribute { min_value: 0.0,
min_value: 0.0, max_value: 1.0,
max_value: 1.0, current_value: 0.08,
current_value: 0.08, default_min_value: 0.0,
default_min_value: 0.0, default_max_value: 1.0,
default_max_value: 1.0, default_value: 0.08,
default_value: 0.08, name: "minecraft:gravity".to_string(),
name: "minecraft:gravity".to_string(), modifiers_list_size: VarUInt(0),
modifiers_list_size: VarUInt(0), },
}, Attribute {
Attribute { min_value: 0.0,
min_value: 0.0, max_value: 400.0,
max_value: 400.0, current_value: 400.0,
current_value: 400.0, default_min_value: 0.0,
default_min_value: 0.0, default_max_value: 400.0,
default_max_value: 400.0, default_value: 400.0,
default_value: 400.0, name: "minecraft:air".to_string(),
name: "minecraft:air".to_string(), modifiers_list_size: VarUInt(0),
modifiers_list_size: VarUInt(0), },
}, Attribute {
Attribute { min_value: 0.0,
min_value: 0.0, max_value: 20.0,
max_value: 20.0, current_value: 20.0,
current_value: 20.0, default_min_value: 0.0,
default_min_value: 0.0, default_max_value: 20.0,
default_max_value: 20.0, default_value: 20.0,
default_value: 20.0, name: "minecraft:health".to_string(),
name: "minecraft:health".to_string(), modifiers_list_size: VarUInt(0),
modifiers_list_size: VarUInt(0), },
}, Attribute {
Attribute { min_value: 0.0,
min_value: 0.0, max_value: 20.0,
max_value: 20.0, current_value: 20.0,
current_value: 20.0, default_min_value: 0.0,
default_min_value: 0.0, default_max_value: 20.0,
default_max_value: 20.0, default_value: 20.0,
default_value: 20.0, name: "minecraft:player.hunger".to_string(),
name: "minecraft:player.hunger".to_string(), modifiers_list_size: VarUInt(0),
modifiers_list_size: VarUInt(0), },
}, ],
], player_tick: VarULong(0),
player_tick: VarULong(0), })
},
&mut frame_set,
)
.await; .await;
client.send_frame_set(frame_set, 0x84).await;
// --- MULTIPLAYER BROADCASTING --- // --- MULTIPLAYER BROADCASTING ---