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::{
connection::SConnectionRequest,
open_connection::{SOpenConnectionRequest1, SOpenConnectionRequest2},
unconnected_ping::SUnconnectedPing,
unconnected_ping::{SUnconnectedPing, SUnconnectedPingOpenConnections},
},
request_chunk_radius::SRequestChunkRadius,
request_network_settings::SRequestNetworkSettings,
@ -60,6 +60,8 @@ fn fuzz_serverbound_packets(payload: &[u8]) {
let _ = SOpenConnectionRequest2::read(&mut cursor);
cursor.set_position(0);
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 available_commands;
pub mod boss_event;
pub mod change_dimension;
pub mod chunk_radius_update;
pub mod common;
pub mod container_open;
@ -50,6 +51,7 @@ pub use add_item_actor::*;
pub use add_player::*;
pub use available_commands::*;
pub use boss_event::*;
pub use change_dimension::*;
pub use chunk_radius_update::*;
pub use common::*;
pub use container_open::*;

View File

@ -2,7 +2,7 @@ use std::net::SocketAddr;
use pumpkin_macros::packet;
use crate::serial::PacketWrite;
use crate::{bedrock::RAKNET_MAGIC, serial::PacketWrite};
#[derive(PacketWrite)]
#[packet(0x03)]
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)?;
if has_net_id {
let _variant = VarUInt::read(buf)?;
let _net_id = VarInt::read(buf)?;
}

View File

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

View File

@ -119,8 +119,12 @@ impl UDPNetworkEncoder {
Ok(())
}
pub const fn encryptor_mut(&mut self) -> Option<&mut BedrockEncryptor> {
self.encryptor.as_mut()
}
pub fn write_game_packet(
&mut self,
&self,
packet_id: u16,
sub_client_sender: SubClient,
sub_client_target: SubClient,
@ -151,26 +155,22 @@ impl UDPNetworkEncoder {
.write_u8(0xfe)
.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 {
// 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));
encoder.write_all(&inner_buffer)?;
let compressed_data = encoder.finish()?;
data_to_encrypt.extend_from_slice(&compressed_data);
data_to_write.extend_from_slice(&compressed_data);
} else {
data_to_encrypt.extend_from_slice(&inner_buffer);
data_to_write.extend_from_slice(&inner_buffer);
}
if let Some(encryptor) = &mut self.encryptor {
encryptor.encrypt(&mut data_to_encrypt);
}
writer.write_all(&data_to_encrypt)?;
writer.write_all(&data_to_write)?;
Ok(())
}

View File

@ -27,6 +27,8 @@ pub struct SPlayerAuthInput {
pub tick: VarULong,
pub delta: Vector3<f32>,
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_unique_id: Option<VarLong>,
pub analog_move: Vector2<f32>,
@ -52,22 +54,18 @@ impl PacketRead for SPlayerAuthInput {
let delta = Vector3::<f32>::read(reader)?;
// 1. Perform Item Interaction
if input_data.get(InputData::PerformItemInteraction as usize) {
// protocol.UseItemTransactionData (Simplified skip)
let _action_type = VarUInt::read(reader)?;
let action_count = VarUInt::read(reader)?.0;
for _ in 0..action_count {
let _type = VarUInt::read(reader)?;
let _pos = BlockPos::read(reader)?;
let _face = VarInt::read(reader)?;
}
}
let item_interaction = if input_data.get(InputData::PerformItemInteraction as usize) {
Some(PlayerInventoryAction::read(reader)?)
} else {
None
};
// 2. Item Stack Request
if input_data.get(InputData::PerformItemStackRequest as usize) {
// protocol.Single ItemStackRequest
return Err(Error::other("ItemStackRequest decoding not implemented"));
}
let item_stack_request = if input_data.get(InputData::PerformItemStackRequest as usize) {
Some(crate::bedrock::server::item_stack_request::ItemStackRequest::read(reader)?)
} else {
None
};
// 3. Block Actions
let block_actions = if input_data.get(InputData::PerformBlockActions as usize) {
@ -109,6 +107,8 @@ impl PacketRead for SPlayerAuthInput {
tick,
delta,
block_actions,
item_interaction,
item_stack_request,
vehicle_rotation,
vehicle_unique_id,
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)]
pub struct PlayerBlockAction {
pub action: VarInt,

View File

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

View File

@ -12,3 +12,14 @@ pub struct SUnconnectedPing {
#[serial(big_endian)]
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>>,
}
#[derive(Debug)]
pub enum BlockPlacingError {
InvalidGamemode,
BlockOutOfWorld,
}
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) {
let ids = T::ids();
let val = Arc::new(block);

View File

@ -2496,21 +2496,42 @@ impl Player {
last_pos.y.round() as i32,
last_pos.z.round() as i32,
));
self.client
.send_packet_now(&CRespawn::new(
PlayerSpawnData::new(
new_world.dimension.clone(),
biome::hash_seed(new_world.level.seed.0), // seed
self.gamemode.load() as u8,
self.previous_gamemode.load().unwrap_or(self.gamemode.load()) as i8,
false,
false,
Some((death_dimension, death_location)),
VarInt(self.get_entity().portal_cooldown.load(Ordering::Relaxed) as i32),
new_world.sea_level.into(),
),
CRespawn::KEEP_ALL_DATA,
)).await;
match self.client.as_ref() {
ClientPlatform::Java(java) => {
java.send_packet_now(&CRespawn::new(
PlayerSpawnData::new(
new_world.dimension.clone(),
biome::hash_seed(new_world.level.seed.0), // seed
self.gamemode.load() as u8,
self.previous_gamemode.load().unwrap_or(self.gamemode.load()) as i8,
false,
false,
Some((death_dimension, death_location)),
VarInt(self.get_entity().portal_cooldown.load(Ordering::Relaxed) as i32),
new_world.sea_level.into(),
),
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();
@ -4931,7 +4952,6 @@ impl InventoryPlayer for Player {
}
ClientPlatform::Bedrock(bedrock) => {
use pumpkin_protocol::bedrock::{
client::inventory_content::CInventoryContent,
client::inventory_slot::CInventorySlot,
network_item::{
ContainerName, FullContainerName, NetworkItemStackDescriptor,
@ -4948,23 +4968,20 @@ impl InventoryPlayer for Player {
if window_id == 0 {
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 main_inventory = &self.inventory().main_inventory;
for s in main_inventory {
let stack = s.lock().await;
slots.push(NetworkItemStackDescriptor::from(&*stack));
}
let slot_idx = packet.slot as usize;
let item_desc = NetworkItemStackDescriptor::from(&*packet.slot_data.0);
let bedrock_packet = CInventoryContent {
container_id: VarUInt(0),
slots,
full_container_name: FullContainerName {
let bedrock_packet = CInventorySlot {
window_id: VarUInt(0),
inventory_slot: VarUInt(slot_idx as u32),
container_name: Some(FullContainerName {
container_name: ContainerName::Inventory,
dynamic_id: None,
},
storage_item: NetworkItemStackDescriptor::default(),
}),
storage: None,
item: item_desc,
};
bedrock.enqueue_packet(&bedrock_packet).await;
} else {
@ -5073,7 +5090,7 @@ impl InventoryPlayer for Player {
}
ClientPlatform::Bedrock(bedrock) => {
use pumpkin_protocol::bedrock::{
client::inventory_content::CInventoryContent,
client::inventory_slot::CInventorySlot,
network_item::{
ContainerName, FullContainerName, NetworkItemStackDescriptor,
},
@ -5081,24 +5098,21 @@ impl InventoryPlayer for Player {
use pumpkin_protocol::codec::var_uint::VarUInt;
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
);
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 {
container_id: VarUInt(0),
slots,
full_container_name: FullContainerName {
let item_stack = &*packet.item.0;
let item_desc = NetworkItemStackDescriptor::from(item_stack);
let bedrock_packet = CInventorySlot {
window_id: VarUInt(0),
inventory_slot: VarUInt(packet.slot.0 as u32),
container_name: Some(FullContainerName {
container_name: ContainerName::Inventory,
dynamic_id: None,
},
storage_item: NetworkItemStackDescriptor::default(),
}),
storage: None,
item: item_desc,
};
bedrock.enqueue_packet(&bedrock_packet).await;
}

View File

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

View File

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

View File

@ -18,14 +18,17 @@ use pumpkin_config::networking::compression::CompressionInfo;
use pumpkin_protocol::{
BClientPacket, PacketDecodeError, RawPacket,
bedrock::{
MTU, RAKNET_ACK, RAKNET_GAME_PACKET, RAKNET_NACK, RakReliability, SPLIT_FRAME_MAX_CONTENT,
SubClient, UDP_HEADER_SIZE,
MTU, RAKNET_ACK, RAKNET_GAME_PACKET, RAKNET_NACK, RAKNET_VALID, RakReliability,
SPLIT_FRAME_MAX_CONTENT, SubClient, UDP_HEADER_SIZE,
ack::Acknowledge,
client::{
disconnect_player::CDisconnectPlayer,
level_chunk::CLevelChunk,
play_status::CPlayStatus,
raknet::connection::CConnectionRequestAccepted,
raknet::connection::{
CAlreadyConnected, CConnectionBanned, CConnectionRequestAccepted, CDisconnect,
CNoFreeIncomingConnections,
},
resource_packs_info::{CResourcePacksInfo, ResourcePackEntry},
},
frame_set::{Frame, FrameSet},
@ -48,10 +51,11 @@ use pumpkin_protocol::{
player_auth_input::SPlayerAuthInput,
raknet::{
connection::{
SConnectedPing, SConnectionRequest, SDisconnect, SNewIncomingConnection,
SConnectedPing, SConnectionLost, SConnectionRequest, SDisconnect,
SNewIncomingConnection,
},
open_connection::{SOpenConnectionRequest1, SOpenConnectionRequest2},
unconnected_ping::SUnconnectedPing,
unconnected_ping::{SUnconnectedPing, SUnconnectedPingOpenConnections},
},
request_ability::SRequestAbility,
request_chunk_radius::SRequestChunkRadius,
@ -241,7 +245,7 @@ impl BedrockClient {
let mut interval = tokio::time::interval(std::time::Duration::from_millis(100));
while !client.close_token.is_cancelled() {
let packet = tokio::select! {
let mut packet = tokio::select! {
biased;
() = client.close_token.cancelled() => break,
res = priority_packet_receiver.recv() => match res {
@ -283,7 +287,7 @@ impl BedrockClient {
}
if !resend.is_empty() {
let encoder = client.network_writer.write().await;
let encoder = client.network_writer.read().await;
for (seq, id, data) in resend {
debug!("Resending reliable sequence {} (ID: {})", seq, id);
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
.send_framed_packet_data(packet.data.to_vec(), RakReliability::ReliableOrdered)
.await;
@ -388,7 +405,7 @@ impl BedrockClient {
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 {
let mut packet_buf = Vec::new();
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");
return;
};
@ -523,7 +540,7 @@ impl BedrockClient {
let mut packet_payload = Vec::new();
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(
P::PACKET_ID as u16,
SubClient::Main,
@ -707,6 +724,10 @@ impl BedrockClient {
return;
}
self.close_token.cancel();
self.send_framed_packet(&CDisconnect, RakReliability::Unreliable)
.await;
self.be_clients.lock().await.remove(&self.address);
}
@ -758,7 +779,7 @@ impl BedrockClient {
RAKNET_NACK => {
self.handle_nack(&Acknowledge::read(reader)?).await;
}
0x80..=0x8d => {
RAKNET_VALID..=0x8d => {
self.handle_frame_set(server, FrameSet::read(reader)?)
.await?;
}
@ -1020,8 +1041,7 @@ impl BedrockClient {
};
let pending = self.pending_profile.lock().await.take();
if let Some((profile, new_config)) = pending {
let mut frame_set = FrameSet::default();
self.write_game_packet_to_set(&CPlayStatus::LoginSuccess, &mut frame_set)
self.enqueue_packet_internal(&CPlayStatus::LoginSuccess)
.await;
let br_config = &server.advanced_config.resource_pack.bedrock;
@ -1052,10 +1072,7 @@ impl BedrockClient {
world_template_version: String::new(),
resource_packs: entries,
};
self.write_game_packet_to_set(&packs_info, &mut frame_set)
.await;
self.send_frame_set(frame_set, 0x84).await;
self.enqueue_packet_internal(&packs_info).await;
return PacketHandlerResult::ReadyToPlay(profile, new_config);
}
error!("Received ClientToServerHandshake but no pending profile was found.");
@ -1226,7 +1243,7 @@ impl BedrockClient {
self.handle_connected_ping(SConnectedPing::read(reader)?)
.await;
}
SDisconnect::PACKET_ID => {
SDisconnect::PACKET_ID | SConnectionLost::PACKET_ID => {
self.close().await;
}
_ => {
@ -1236,6 +1253,7 @@ impl BedrockClient {
Ok(())
}
#[expect(clippy::too_many_lines)]
pub async fn handle_offline_packet(
server: &Server,
packet_id: u8,
@ -1246,6 +1264,38 @@ impl BedrockClient {
) -> Result<(), Error> {
let packet_id_i32 = i32::from(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 mut clients_guard = be_clients.lock().await;
clients_guard.remove(&addr)
@ -1269,6 +1319,20 @@ impl BedrockClient {
)
.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 => {
Self::handle_open_connection_1(
server,
@ -1279,6 +1343,20 @@ impl BedrockClient {
.await;
}
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(
server,
SOpenConnectionRequest2::read(payload)?,

View File

@ -57,21 +57,45 @@ use crate::{
use pumpkin_data::BlockDirection;
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 {
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 {
tracing::warn!(
"Failed to map bedrock item id {} and data {} to Java item",
desc.id.0,
desc.aux_value.0
);
ItemStack::EMPTY.clone()
let mut mapped_item = None;
if is_creative {
let index = (desc.id.0.saturating_sub(1)) as usize;
if index < pumpkin_data::bedrock_creative::CREATIVE_ENTRIES.len() {
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) {
let mut abilities = player.abilities.lock().await;
if !abilities.flying {
let flying = { player.abilities.lock().await.flying };
if !flying {
send_cancellable! {{
server;
PlayerToggleFlightEvent::new(player.clone(), true);
'after: {
abilities.flying = true;
{
player.abilities.lock().await.flying = true;
};
player.send_abilities_update().await;
}
'cancelled: {
@ -336,13 +362,15 @@ impl BedrockClient {
}}
}
} else if input_data.get(InputData::StopFlying as usize) {
let mut abilities = player.abilities.lock().await;
if abilities.flying {
let flying = { player.abilities.lock().await.flying };
if flying {
send_cancellable! {{
server;
PlayerToggleFlightEvent::new(player.clone(), false);
'after: {
abilities.flying = false;
{
player.abilities.lock().await.flying = false;
};
player.send_abilities_update().await;
}
'cancelled: {
@ -504,12 +532,13 @@ impl BedrockClient {
player_screen_handler.send_content_updates().await;
}
let is_creative = player.gamemode.load() == GameMode::Creative;
for action in &packet.actions {
use pumpkin_protocol::bedrock::server::inventory_transaction::InventoryActionSource;
let source_type = InventoryActionSource::from(action.source_type);
if source_type == InventoryActionSource::World {
let old_stack = descriptor_to_stack(&action.old_item);
let new_stack = descriptor_to_stack(&action.new_item);
let old_stack = descriptor_to_stack(&action.old_item, is_creative);
let new_stack = descriptor_to_stack(&action.new_item, is_creative);
if old_stack.is_empty() && !new_stack.is_empty() {
player.drop_item(new_stack).await;
}
@ -517,7 +546,7 @@ impl BedrockClient {
if let Some(screen_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;
@ -610,7 +639,16 @@ impl BedrockClient {
if data.action_type.0 == 0 {
// 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();
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
.block_registry
@ -648,6 +686,53 @@ impl BedrockClient {
)
.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) => {

View File

@ -12,7 +12,7 @@ use tracing::{Level, debug, error, info, trace, warn};
use crate::block::BlockHitResult;
use crate::block::registry::BlockActionResult;
use crate::block::{self, BlockIsReplacing};
use crate::block::{self};
use crate::entity::EntityBase;
use crate::entity::equipment_break_status;
use crate::entity::player::statistics::{CustomStatistic, StatisticCategory};
@ -21,7 +21,6 @@ use crate::error::PumpkinError;
use crate::log_at_level;
use crate::net::PlayerConfig;
use crate::net::java::JavaClient;
use crate::plugin::block::block_place::BlockPlaceEvent;
use crate::plugin::player::changed_main_hand::PlayerChangedMainHandEvent;
use crate::plugin::player::fish::{PlayerFishEvent, PlayerFishState};
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::server::{Server, seasonal_events};
use crate::world::{World, chunker};
use pumpkin_data::block_properties::{
BlockProperties, CommandBlockLikeProperties, WaterLikeProperties,
};
use pumpkin_data::block_properties::{BlockProperties, CommandBlockLikeProperties};
use pumpkin_data::data_component_impl::{
BlocksAttacksImpl, ConsumableImpl, EquipmentSlot, EquippableImpl, FoodImpl,
};
use pumpkin_data::entity::EntityType;
use pumpkin_data::item::Item;
use pumpkin_data::item_stack::ItemStack;
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::merchant::merchant_screen_handler::MerchantScreenHandler;
use pumpkin_inventory::player::player_inventory::PlayerInventory;
@ -77,10 +73,8 @@ use pumpkin_protocol::java::server::play::{
SSetJigsawBlock, SSetPlayerGround, SSetTestBlock, SSwingArm, STeleportToEntity,
STestInstanceBlockAction, SUpdateSign, SUseItem, SUseItemOn, Status,
};
use pumpkin_util::math::boundingbox::BoundingBox;
use pumpkin_util::math::vector3::Vector3;
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_world::generation::structure::structures::jigsaw::JigsawJointType;
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(
&self,
player: &Arc<Player>,
@ -2769,171 +2714,27 @@ impl JavaClient {
location: BlockPos,
face: BlockDirection,
) -> Result<bool, BlockPlacingError> {
let entity = &player.get_entity();
match player.gamemode.load() {
GameMode::Spectator | GameMode::Adventure => {
return Err(BlockPlacingError::InvalidGamemode);
}
_ => {}
}
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,
)
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;
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 == 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)
Ok(true)
}
} 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 == 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);
Ok(None) => Ok(false),
Err(crate::block::registry::BlockPlacingError::InvalidGamemode) => {
Err(BlockPlacingError::InvalidGamemode)
}
Err(crate::block::registry::BlockPlacingError::BlockOutOfWorld) => {
Err(BlockPlacingError::BlockOutOfWorld)
}
}
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.

View File

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