From e1967bed2b94f3eaf6825c01f4b12a11122397f9 Mon Sep 17 00:00:00 2001 From: Alexander Medvedev Date: Mon, 25 May 2026 11:53:37 +0200 Subject: [PATCH] feat: bedrock & java cross play --- Cargo.lock | 64 +++ .../src/bedrock/client/add_actor.rs | 98 ++++ .../src/bedrock/client/add_player.rs | 147 +++++ pumpkin-protocol/src/bedrock/client/common.rs | 21 + pumpkin-protocol/src/bedrock/client/mod.rs | 14 + .../src/bedrock/client/move_actor_absolute.rs | 39 ++ .../src/bedrock/client/move_actor_delta.rs | 78 +++ .../src/bedrock/client/move_player.rs | 60 +- .../src/bedrock/client/player_list.rs | 204 +++++++ .../src/bedrock/client/remove_actor.rs | 15 + .../src/bedrock/client/set_actor_motion.rs | 11 + .../src/bedrock/client/update_abilities.rs | 13 +- .../src/bedrock/server/animate.rs | 115 ++-- .../src/bedrock/server/interaction.rs | 4 + .../src/bedrock/server/player_auth_input.rs | 2 +- pumpkin-protocol/src/codec/var_long.rs | 2 +- pumpkin-protocol/src/codec/var_ulong.rs | 2 +- pumpkin/Cargo.toml | 1 + pumpkin/src/entity/ai/goal/melee_attack.rs | 2 +- pumpkin/src/entity/living.rs | 28 +- pumpkin/src/entity/mod.rs | 232 ++++++-- pumpkin/src/entity/player.rs | 99 +++- pumpkin/src/lib.rs | 40 +- pumpkin/src/net/bedrock/login.rs | 53 +- pumpkin/src/net/bedrock/mod.rs | 264 ++++++--- pumpkin/src/net/bedrock/play.rs | 190 ++++++- pumpkin/src/net/java/mod.rs | 9 +- pumpkin/src/net/java/play.rs | 119 ++-- pumpkin/src/net/mod.rs | 62 ++- pumpkin/src/world/mod.rs | 516 ++++++++++++++++-- 30 files changed, 2180 insertions(+), 324 deletions(-) create mode 100644 pumpkin-protocol/src/bedrock/client/add_actor.rs create mode 100644 pumpkin-protocol/src/bedrock/client/add_player.rs create mode 100644 pumpkin-protocol/src/bedrock/client/common.rs create mode 100644 pumpkin-protocol/src/bedrock/client/move_actor_absolute.rs create mode 100644 pumpkin-protocol/src/bedrock/client/move_actor_delta.rs create mode 100644 pumpkin-protocol/src/bedrock/client/player_list.rs create mode 100644 pumpkin-protocol/src/bedrock/client/remove_actor.rs diff --git a/Cargo.lock b/Cargo.lock index 36bdaba22..93df60cb7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -250,12 +250,24 @@ dependencies = [ "allocator-api2", ] +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" + [[package]] name = "byteorder" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + [[package]] name = "bytes" version = "1.11.1" @@ -1173,6 +1185,15 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + [[package]] name = "ff" version = "0.13.1" @@ -1775,6 +1796,19 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "image" +version = "0.25.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" +dependencies = [ + "bytemuck", + "byteorder-lite", + "moxcms", + "num-traits", + "png", +] + [[package]] name = "indexmap" version = "2.14.0" @@ -2091,6 +2125,16 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "moxcms" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" +dependencies = [ + "num-traits", + "pxfm", +] + [[package]] name = "nix" version = "0.31.3" @@ -2472,6 +2516,19 @@ version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + [[package]] name = "postcard" version = "1.1.3" @@ -2619,6 +2676,7 @@ dependencies = [ "futures", "hmac 0.13.0", "hyper", + "image", "libloading", "notify", "num-bigint", @@ -2841,6 +2899,12 @@ dependencies = [ "xxhash-rust", ] +[[package]] +name = "pxfm" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0c5ccf5294c6ccd63a74f1565028353830a9c2f5eb0c682c355c471726a6e3f" + [[package]] name = "quote" version = "1.0.45" diff --git a/pumpkin-protocol/src/bedrock/client/add_actor.rs b/pumpkin-protocol/src/bedrock/client/add_actor.rs new file mode 100644 index 000000000..7034e11bf --- /dev/null +++ b/pumpkin-protocol/src/bedrock/client/add_actor.rs @@ -0,0 +1,98 @@ +use crate::{ + codec::{var_long::VarLong, var_uint::VarUInt, var_ulong::VarULong}, + serial::PacketWrite, +}; +use pumpkin_macros::packet; +use pumpkin_util::math::vector3::Vector3; +use std::io::{Error, Write}; + +use super::{ + common::EntityLink, + set_actor_data::{EntityMetadata, PropertySyncData}, +}; + +#[packet(13)] +pub struct CAddActor { + pub entity_unique_id: VarLong, + pub entity_runtime_id: VarULong, + pub entity_type: String, + pub position: Vector3, + pub velocity: Vector3, + pub pitch: f32, + pub yaw: f32, + pub head_yaw: f32, + pub body_yaw: f32, + pub attributes: Vec, + pub metadata: EntityMetadata, + pub synced_properties: PropertySyncData, + pub links: Vec, +} + +impl PacketWrite for CAddActor { + fn write(&self, writer: &mut W) -> Result<(), Error> { + self.entity_unique_id.write(writer)?; + self.entity_runtime_id.write(writer)?; + self.entity_type.write(writer)?; + self.position.write(writer)?; + self.velocity.write(writer)?; + self.pitch.write(writer)?; + self.yaw.write(writer)?; + self.head_yaw.write(writer)?; + self.body_yaw.write(writer)?; + VarUInt(self.attributes.len() as u32).write(writer)?; + for attr in &self.attributes { + attr.write(writer)?; + } + self.metadata.write(writer)?; + self.synced_properties.write(writer)?; + VarUInt(self.links.len() as u32).write(writer)?; + for link in &self.links { + link.write(writer)?; + } + Ok(()) + } +} + +impl CAddActor { + #[allow(clippy::too_many_arguments)] + #[must_use] + pub const fn new( + entity_unique_id: VarLong, + entity_runtime_id: VarULong, + entity_type: String, + position: Vector3, + velocity: Vector3, + pitch: f32, + yaw: f32, + head_yaw: f32, + body_yaw: f32, + attributes: Vec, + metadata: EntityMetadata, + synced_properties: PropertySyncData, + links: Vec, + ) -> Self { + Self { + entity_unique_id, + entity_runtime_id, + entity_type, + position, + velocity, + pitch, + yaw, + head_yaw, + body_yaw, + attributes, + metadata, + synced_properties, + links, + } + } +} + +#[derive(PacketWrite)] +pub struct AttributeValue { + pub name: String, + pub min: f32, + pub value: f32, + pub max: f32, +} diff --git a/pumpkin-protocol/src/bedrock/client/add_player.rs b/pumpkin-protocol/src/bedrock/client/add_player.rs new file mode 100644 index 000000000..d3e1c6404 --- /dev/null +++ b/pumpkin-protocol/src/bedrock/client/add_player.rs @@ -0,0 +1,147 @@ +use crate::{ + bedrock::network_item::NetworkItemDescriptor, + codec::{var_int::VarInt, var_uint::VarUInt, var_ulong::VarULong}, + serial::PacketWrite, +}; +use pumpkin_macros::packet; +use pumpkin_util::math::vector3::Vector3; +use std::io::{Error, Write}; +use uuid::Uuid; + +use super::{ + common::{AbilityLayer, EntityLink}, + set_actor_data::EntityMetadata, +}; + +#[packet(12)] +pub struct CAddPlayer { + pub uuid: Uuid, + pub username: String, + pub entity_runtime_id: VarULong, + pub platform_chat_id: String, + pub position: Vector3, + pub velocity: Vector3, + pub pitch: f32, + pub yaw: f32, + pub head_yaw: f32, + pub held_item: NetworkItemDescriptor, + pub game_mode: VarInt, + pub metadata: EntityMetadata, + pub properties: EntityProperties, + pub ability_data: AbilityData, + pub links: Vec, + pub device_id: String, + pub build_platform: i32, +} + +impl PacketWrite for CAddPlayer { + fn write(&self, writer: &mut W) -> Result<(), Error> { + self.uuid.write(writer)?; + self.username.write(writer)?; + self.entity_runtime_id.write(writer)?; + self.platform_chat_id.write(writer)?; + self.position.write(writer)?; + self.velocity.write(writer)?; + self.pitch.write(writer)?; + self.yaw.write(writer)?; + self.head_yaw.write(writer)?; + self.held_item.write(writer)?; + self.game_mode.write(writer)?; + self.metadata.write(writer)?; + self.properties.write(writer)?; + self.ability_data.write(writer)?; + VarUInt(self.links.len() as u32).write(writer)?; + for link in &self.links { + link.write(writer)?; + } + self.device_id.write(writer)?; + self.build_platform.write(writer) + } +} + +impl CAddPlayer { + #[allow(clippy::too_many_arguments)] + #[must_use] + pub const fn new( + uuid: Uuid, + username: String, + entity_runtime_id: VarULong, + platform_chat_id: String, + position: Vector3, + velocity: Vector3, + pitch: f32, + yaw: f32, + head_yaw: f32, + held_item: NetworkItemDescriptor, + game_mode: VarInt, + metadata: EntityMetadata, + properties: EntityProperties, + ability_data: AbilityData, + links: Vec, + device_id: String, + build_platform: i32, + ) -> Self { + Self { + uuid, + username, + entity_runtime_id, + platform_chat_id, + position, + velocity, + pitch, + yaw, + head_yaw, + held_item, + game_mode, + metadata, + properties, + ability_data, + links, + device_id, + build_platform, + } + } +} + +#[derive(Default, Clone)] +pub struct EntityProperties { + pub ints: Vec<(VarUInt, VarInt)>, + pub floats: Vec<(VarUInt, f32)>, +} + +impl PacketWrite for EntityProperties { + fn write(&self, writer: &mut W) -> Result<(), Error> { + VarUInt(self.ints.len() as u32).write(writer)?; + for (id, val) in &self.ints { + id.write(writer)?; + val.write(writer)?; + } + VarUInt(self.floats.len() as u32).write(writer)?; + for (id, val) in &self.floats { + id.write(writer)?; + val.write(writer)?; + } + Ok(()) + } +} + +#[derive(Default, Clone)] +pub struct AbilityData { + pub entity_unique_id: i64, + pub player_permissions: u8, + pub command_permissions: u8, + pub layers: Vec, +} + +impl PacketWrite for AbilityData { + fn write(&self, writer: &mut W) -> Result<(), Error> { + self.entity_unique_id.write(writer)?; + self.player_permissions.write(writer)?; + self.command_permissions.write(writer)?; + (self.layers.len() as u8).write(writer)?; + for layer in &self.layers { + layer.write(writer)?; + } + Ok(()) + } +} diff --git a/pumpkin-protocol/src/bedrock/client/common.rs b/pumpkin-protocol/src/bedrock/client/common.rs new file mode 100644 index 000000000..3651e6fa6 --- /dev/null +++ b/pumpkin-protocol/src/bedrock/client/common.rs @@ -0,0 +1,21 @@ +use crate::{codec::var_long::VarLong, serial::PacketWrite}; + +#[derive(Default, Clone, PacketWrite)] +pub struct AbilityLayer { + pub serialized_layer: u16, + pub abilities_set: u32, + pub ability_value: u32, + pub fly_speed: f32, + pub vertical_fly_speed: f32, + pub walk_speed: f32, +} + +#[derive(Default, Clone, PacketWrite)] +pub struct EntityLink { + pub ridden_unique_id: VarLong, + pub rider_unique_id: VarLong, + pub link_type: u8, + pub immediate: bool, + pub rider_initiated: bool, + pub vehicle_angular_velocity: f32, +} diff --git a/pumpkin-protocol/src/bedrock/client/mod.rs b/pumpkin-protocol/src/bedrock/client/mod.rs index c6484c1cd..a3123735e 100644 --- a/pumpkin-protocol/src/bedrock/client/mod.rs +++ b/pumpkin-protocol/src/bedrock/client/mod.rs @@ -1,6 +1,9 @@ +pub mod add_actor; +pub mod add_player; pub mod available_commands; pub mod boss_event; pub mod chunk_radius_update; +pub mod common; pub mod container_open; pub mod correct_player_move; pub mod creative_content; @@ -11,12 +14,16 @@ pub mod inventory_content; pub mod level_chunk; pub mod level_event; pub mod modal_form_request; +pub mod move_actor_absolute; +pub mod move_actor_delta; pub mod move_player; pub mod network_chunk_publisher_update; pub mod network_settings; pub mod play_status; pub mod player_hotbar; +pub mod player_list; pub mod raknet; +pub mod remove_actor; pub mod resource_pack_stack; pub mod resource_packs_info; pub mod scoreboard; @@ -31,9 +38,12 @@ pub mod transfer; pub mod update_abilities; pub mod update_attributes; +pub use add_actor::*; +pub use add_player::*; pub use available_commands::*; pub use boss_event::*; pub use chunk_radius_update::*; +pub use common::*; pub use container_open::*; pub use correct_player_move::*; pub use creative_content::*; @@ -44,12 +54,16 @@ pub use inventory_content::*; pub use level_chunk::*; pub use level_event::*; pub use modal_form_request::*; +pub use move_actor_absolute::*; +pub use move_actor_delta::*; pub use move_player::*; pub use network_chunk_publisher_update::*; pub use network_settings::*; pub use play_status::*; pub use player_hotbar::*; +pub use player_list::*; pub use raknet::*; +pub use remove_actor::*; pub use resource_pack_stack::*; pub use resource_packs_info::*; pub use scoreboard::*; diff --git a/pumpkin-protocol/src/bedrock/client/move_actor_absolute.rs b/pumpkin-protocol/src/bedrock/client/move_actor_absolute.rs new file mode 100644 index 000000000..f8753faff --- /dev/null +++ b/pumpkin-protocol/src/bedrock/client/move_actor_absolute.rs @@ -0,0 +1,39 @@ +use crate::{codec::var_ulong::VarULong, serial::PacketWrite}; +use pumpkin_macros::packet; +use pumpkin_util::math::vector3::Vector3; + +#[derive(PacketWrite)] +#[packet(18)] +pub struct CMoveActorAbsolute { + pub entity_runtime_id: VarULong, + pub flags: u8, + pub position: Vector3, + pub pitch: u8, + pub yaw: u8, + pub head_yaw: u8, +} + +impl CMoveActorAbsolute { + pub const FLAG_ON_GROUND: u8 = 0x01; + pub const FLAG_TELEPORT: u8 = 0x02; + pub const FLAG_FORCE_MOVE: u8 = 0x04; + + #[must_use] + pub const fn new( + entity_runtime_id: VarULong, + flags: u8, + position: Vector3, + pitch: u8, + yaw: u8, + head_yaw: u8, + ) -> Self { + Self { + entity_runtime_id, + flags, + position, + pitch, + yaw, + head_yaw, + } + } +} diff --git a/pumpkin-protocol/src/bedrock/client/move_actor_delta.rs b/pumpkin-protocol/src/bedrock/client/move_actor_delta.rs new file mode 100644 index 000000000..2c0463274 --- /dev/null +++ b/pumpkin-protocol/src/bedrock/client/move_actor_delta.rs @@ -0,0 +1,78 @@ +use crate::{codec::var_ulong::VarULong, serial::PacketWrite}; +use pumpkin_macros::packet; +use std::io::{Error, Write}; + +pub const MOVE_ACTOR_DELTA_FLAG_HAS_X: u16 = 0x0001; +pub const MOVE_ACTOR_DELTA_FLAG_HAS_Y: u16 = 0x0002; +pub const MOVE_ACTOR_DELTA_FLAG_HAS_Z: u16 = 0x0004; +pub const MOVE_ACTOR_DELTA_FLAG_HAS_PITCH: u16 = 0x0008; +pub const MOVE_ACTOR_DELTA_FLAG_HAS_YAW: u16 = 0x0010; +pub const MOVE_ACTOR_DELTA_FLAG_HAS_HEAD_YAW: u16 = 0x0020; +pub const MOVE_ACTOR_DELTA_FLAG_ON_GROUND: u16 = 0x0040; +pub const MOVE_ACTOR_DELTA_FLAG_TELEPORT: u16 = 0x0080; +pub const MOVE_ACTOR_DELTA_FLAG_FORCE_MOVE: u16 = 0x0100; + +#[packet(111)] +pub struct CMoveActorDelta { + pub entity_runtime_id: VarULong, + pub flags: u16, + pub x: f32, + pub y: f32, + pub z: f32, + pub pitch: u8, + pub yaw: u8, + pub head_yaw: u8, +} + +impl CMoveActorDelta { + #[allow(clippy::too_many_arguments)] + #[must_use] + pub const fn new( + entity_runtime_id: VarULong, + flags: u16, + x: f32, + y: f32, + z: f32, + pitch: u8, + yaw: u8, + head_yaw: u8, + ) -> Self { + Self { + entity_runtime_id, + flags, + x, + y, + z, + pitch, + yaw, + head_yaw, + } + } +} + +impl PacketWrite for CMoveActorDelta { + fn write(&self, writer: &mut W) -> Result<(), Error> { + self.entity_runtime_id.write(writer)?; + self.flags.write(writer)?; + + if self.flags & MOVE_ACTOR_DELTA_FLAG_HAS_X != 0 { + self.x.write(writer)?; + } + if self.flags & MOVE_ACTOR_DELTA_FLAG_HAS_Y != 0 { + self.y.write(writer)?; + } + if self.flags & MOVE_ACTOR_DELTA_FLAG_HAS_Z != 0 { + self.z.write(writer)?; + } + if self.flags & MOVE_ACTOR_DELTA_FLAG_HAS_PITCH != 0 { + self.pitch.write(writer)?; + } + if self.flags & MOVE_ACTOR_DELTA_FLAG_HAS_YAW != 0 { + self.yaw.write(writer)?; + } + if self.flags & MOVE_ACTOR_DELTA_FLAG_HAS_HEAD_YAW != 0 { + self.head_yaw.write(writer)?; + } + Ok(()) + } +} diff --git a/pumpkin-protocol/src/bedrock/client/move_player.rs b/pumpkin-protocol/src/bedrock/client/move_player.rs index e2b29552a..df96629e9 100644 --- a/pumpkin-protocol/src/bedrock/client/move_player.rs +++ b/pumpkin-protocol/src/bedrock/client/move_player.rs @@ -1,12 +1,11 @@ +use crate::{codec::var_ulong::VarULong, serial::PacketWrite}; use pumpkin_macros::packet; use pumpkin_util::math::vector3::Vector3; +use std::io::{Error, Write}; -use crate::{codec::var_ulong::VarULong, serial::PacketWrite}; - -#[derive(Debug, PacketWrite)] +#[derive(Debug)] #[packet(19)] pub struct CMovePlayer { - // https://mojang.github.io/bedrock-protocol-docs/html/MovePlayerPacket.html pub player_runtime_id: VarULong, pub position: Vector3, pub pitch: f32, @@ -15,6 +14,8 @@ pub struct CMovePlayer { pub mode: u8, pub on_ground: bool, pub riding_runtime_id: VarULong, + pub teleport_cause: i32, + pub teleport_source_entity_type: i32, pub tick: VarULong, } @@ -22,5 +23,54 @@ impl CMovePlayer { pub const MODE_NORMAL: u8 = 0; pub const MODE_RESET: u8 = 1; pub const MODE_TELEPORT: u8 = 2; - pub const MODE_PITCH: u8 = 3; + pub const MODE_ROTATION: u8 = 3; + + #[allow(clippy::too_many_arguments)] + #[must_use] + pub const fn new( + player_runtime_id: VarULong, + position: Vector3, + pitch: f32, + yaw: f32, + head_yaw: f32, + mode: u8, + on_ground: bool, + riding_runtime_id: VarULong, + teleport_cause: i32, + teleport_source_entity_type: i32, + tick: VarULong, + ) -> Self { + Self { + player_runtime_id, + position, + pitch, + yaw, + head_yaw, + mode, + on_ground, + riding_runtime_id, + teleport_cause, + teleport_source_entity_type, + tick, + } + } +} + +impl PacketWrite for CMovePlayer { + fn write(&self, writer: &mut W) -> Result<(), Error> { + self.player_runtime_id.write(writer)?; + self.position.write(writer)?; + self.pitch.write(writer)?; + self.yaw.write(writer)?; + self.head_yaw.write(writer)?; + self.mode.write(writer)?; + self.on_ground.write(writer)?; + self.riding_runtime_id.write(writer)?; + if self.mode == Self::MODE_TELEPORT { + self.teleport_cause.write(writer)?; + self.teleport_source_entity_type.write(writer)?; + } + self.tick.write(writer)?; + Ok(()) + } } diff --git a/pumpkin-protocol/src/bedrock/client/player_list.rs b/pumpkin-protocol/src/bedrock/client/player_list.rs new file mode 100644 index 000000000..3d656e07c --- /dev/null +++ b/pumpkin-protocol/src/bedrock/client/player_list.rs @@ -0,0 +1,204 @@ +use crate::{ + codec::{var_long::VarLong, var_uint::VarUInt}, + serial::PacketWrite, +}; +use pumpkin_macros::packet; +use std::io::{Error, Write}; +use uuid::Uuid; + +#[packet(63)] +pub struct CPlayerList { + pub action: u8, + pub entries: Vec, +} + +impl PacketWrite for CPlayerList { + fn write(&self, writer: &mut W) -> Result<(), Error> { + self.action.write(writer)?; + VarUInt(self.entries.len() as u32).write(writer)?; + match self.action { + Self::ACTION_ADD => { + for entry in &self.entries { + entry.write(writer)?; + } + for entry in &self.entries { + entry.skin.is_trusted.write(writer)?; + } + } + Self::ACTION_REMOVE => { + for entry in &self.entries { + entry.uuid.write(writer)?; + } + } + _ => return Err(Error::other("Invalid PlayerList action")), + } + Ok(()) + } +} + +impl CPlayerList { + pub const ACTION_ADD: u8 = 0; + pub const ACTION_REMOVE: u8 = 1; +} + +pub struct PlayerListEntry { + pub uuid: Uuid, + pub entity_unique_id: VarLong, + pub username: String, + pub xuid: String, + pub platform_chat_id: String, + pub build_platform: i32, + pub skin: Skin, + pub is_teacher: bool, + pub is_host: bool, + pub is_sub_client: bool, + pub player_color: [u8; 4], // ARGB +} + +impl PacketWrite for PlayerListEntry { + fn write(&self, writer: &mut W) -> Result<(), Error> { + self.uuid.write(writer)?; + self.entity_unique_id.write(writer)?; + self.username.write(writer)?; + self.xuid.write(writer)?; + self.platform_chat_id.write(writer)?; + self.build_platform.write(writer)?; + self.skin.write(writer)?; + self.is_teacher.write(writer)?; + self.is_host.write(writer)?; + self.is_sub_client.write(writer)?; + self.player_color.write(writer) + } +} + +#[derive(Clone)] +pub struct Skin { + pub skin_id: String, + pub play_fab_id: String, + pub resource_patch: Vec, + pub image_width: u32, + pub image_height: u32, + pub skin_data: Vec, + pub animations: Vec, + pub cape_width: u32, + pub cape_height: u32, + pub cape_data: Vec, + pub geometry_data: Vec, + pub animation_data: Vec, + pub geometry_data_engine_version: Vec, + pub cape_id: String, + pub full_id: String, + pub arm_size: String, + pub skin_color: String, + pub persona_pieces: Vec, + pub piece_tint_colors: Vec, + pub is_premium: bool, + pub is_persona: bool, + pub persona_cape_on_classic: bool, + pub is_primary_user: bool, + pub override_appearance: bool, + pub is_trusted: bool, +} + +impl Skin { + #[must_use] + pub fn steve() -> Self { + Self { + skin_id: "Standard_Custom".to_string(), + play_fab_id: String::new(), + resource_patch: r#"{"geometry":{"default":"geometry.humanoid.custom"}}"#.into(), + image_width: 64, + image_height: 64, + // 64 * 64 * 4 = 16384 bytes of raw RGBA data + skin_data: vec![0; 16384], + animations: Vec::new(), + cape_width: 0, + cape_height: 0, + cape_data: Vec::new(), + geometry_data: Vec::new(), + animation_data: Vec::new(), + geometry_data_engine_version: Vec::new(), + cape_id: String::new(), + full_id: "Standard_Custom".to_string(), + arm_size: "wide".to_string(), + skin_color: "#0".to_string(), + persona_pieces: Vec::new(), + piece_tint_colors: Vec::new(), + is_premium: false, + is_persona: false, + persona_cape_on_classic: false, + is_primary_user: false, + override_appearance: false, + is_trusted: true, + } + } +} + +impl PacketWrite for Skin { + fn write(&self, writer: &mut W) -> Result<(), Error> { + self.skin_id.write(writer)?; + self.play_fab_id.write(writer)?; + VarUInt(self.resource_patch.len() as u32).write(writer)?; + writer.write_all(&self.resource_patch)?; + self.image_width.write(writer)?; + self.image_height.write(writer)?; + VarUInt(self.skin_data.len() as u32).write(writer)?; + writer.write_all(&self.skin_data)?; + (self.animations.len() as u32).write(writer)?; + for anim in &self.animations { + anim.write(writer)?; + } + self.cape_width.write(writer)?; + self.cape_height.write(writer)?; + VarUInt(self.cape_data.len() as u32).write(writer)?; + writer.write_all(&self.cape_data)?; + VarUInt(self.geometry_data.len() as u32).write(writer)?; + writer.write_all(&self.geometry_data)?; + VarUInt(self.animation_data.len() as u32).write(writer)?; + writer.write_all(&self.animation_data)?; + VarUInt(self.geometry_data_engine_version.len() as u32).write(writer)?; + writer.write_all(&self.geometry_data_engine_version)?; + self.cape_id.write(writer)?; + self.full_id.write(writer)?; + self.arm_size.write(writer)?; + self.skin_color.write(writer)?; + (self.persona_pieces.len() as u32).write(writer)?; + for piece in &self.persona_pieces { + piece.write(writer)?; + } + (self.piece_tint_colors.len() as u32).write(writer)?; + for color in &self.piece_tint_colors { + color.write(writer)?; + } + self.is_premium.write(writer)?; + self.is_persona.write(writer)?; + self.persona_cape_on_classic.write(writer)?; + self.is_primary_user.write(writer)?; + self.override_appearance.write(writer) + } +} + +#[derive(Clone, PacketWrite)] +pub struct SkinAnimation { + pub image_width: u32, + pub image_height: u32, + pub image_data: Vec, + pub animation_type: u32, + pub frames: f32, + pub expression_type: u32, +} + +#[derive(Clone, PacketWrite)] +pub struct PersonaPiece { + pub piece_id: String, + pub piece_type: String, + pub pack_id: String, + pub is_default: bool, + pub product_id: String, +} + +#[derive(Clone, PacketWrite)] +pub struct PieceTintColor { + pub piece_type: String, + pub colors: Vec, +} diff --git a/pumpkin-protocol/src/bedrock/client/remove_actor.rs b/pumpkin-protocol/src/bedrock/client/remove_actor.rs new file mode 100644 index 000000000..ab7c44abc --- /dev/null +++ b/pumpkin-protocol/src/bedrock/client/remove_actor.rs @@ -0,0 +1,15 @@ +use crate::{codec::var_long::VarLong, serial::PacketWrite}; +use pumpkin_macros::packet; + +#[derive(PacketWrite)] +#[packet(14)] +pub struct CRemoveActor { + pub entity_unique_id: VarLong, +} + +impl CRemoveActor { + #[must_use] + pub const fn new(entity_unique_id: VarLong) -> Self { + Self { entity_unique_id } + } +} diff --git a/pumpkin-protocol/src/bedrock/client/set_actor_motion.rs b/pumpkin-protocol/src/bedrock/client/set_actor_motion.rs index eed5ec727..50d585589 100644 --- a/pumpkin-protocol/src/bedrock/client/set_actor_motion.rs +++ b/pumpkin-protocol/src/bedrock/client/set_actor_motion.rs @@ -10,3 +10,14 @@ pub struct CSetActorMotion { pub motion: Vector3, pub tick: VarULong, } + +impl CSetActorMotion { + #[must_use] + pub const fn new(target_runtime_id: VarULong, motion: Vector3, tick: VarULong) -> Self { + Self { + target_runtime_id, + motion, + tick, + } + } +} diff --git a/pumpkin-protocol/src/bedrock/client/update_abilities.rs b/pumpkin-protocol/src/bedrock/client/update_abilities.rs index 10e3e4116..67201b8ea 100644 --- a/pumpkin-protocol/src/bedrock/client/update_abilities.rs +++ b/pumpkin-protocol/src/bedrock/client/update_abilities.rs @@ -2,6 +2,8 @@ use pumpkin_macros::packet; use crate::serial::PacketWrite; +use super::common::AbilityLayer; + #[packet(187)] #[derive(PacketWrite)] pub struct CUpdateAbilities { @@ -13,17 +15,6 @@ pub struct CUpdateAbilities { pub layers: Vec, } -#[derive(PacketWrite)] -pub struct AbilityLayer { - // https://mojang.github.io/bedrock-protocol-docs/html/SerializedAbilitiesData__SerializedLayer.html - pub serialized_layer: u16, - pub abilities_set: u32, - pub ability_value: u32, - pub fly_speed: f32, - pub vertical_fly_speed: f32, - pub walk_speed: f32, -} - #[repr(u32)] pub enum Ability { Build = 0, diff --git a/pumpkin-protocol/src/bedrock/server/animate.rs b/pumpkin-protocol/src/bedrock/server/animate.rs index 20aabc2f2..191664d5f 100644 --- a/pumpkin-protocol/src/bedrock/server/animate.rs +++ b/pumpkin-protocol/src/bedrock/server/animate.rs @@ -1,4 +1,7 @@ -use std::io::{Error, Read, Write}; +use std::{ + io::{Error, Read, Write}, + str::FromStr, +}; use pumpkin_macros::packet; @@ -7,36 +10,24 @@ use crate::{ serial::{PacketRead, PacketWrite}, }; -#[derive(Debug)] -#[packet(44)] -pub struct SAnimate { - pub action: AnimateAction, - pub runtime_entity_id: VarULong, - pub boat_rowing_time: Option, -} - #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum AnimateAction { - NoAction = 0, SwingArm = 1, WakeUp = 2, - CriticalHit = 3, - MagicCriticalHit = 4, - RowRight = 128, - RowLeft = 129, + StopSleep = 3, + CriticalHit = 4, + MagicCriticalHit = 5, } impl PacketRead for AnimateAction { fn read(reader: &mut R) -> Result { let action = u8::read(reader)?; match action { - 0 => Ok(Self::NoAction), 1 => Ok(Self::SwingArm), 2 => Ok(Self::WakeUp), - 3 => Ok(Self::CriticalHit), - 4 => Ok(Self::MagicCriticalHit), - 128 => Ok(Self::RowRight), - 129 => Ok(Self::RowLeft), + 3 => Ok(Self::StopSleep), + 4 => Ok(Self::CriticalHit), + 5 => Ok(Self::MagicCriticalHit), _ => Err(Error::other(format!("Invalid animate action ID: {action}"))), } } @@ -48,21 +39,81 @@ impl PacketWrite for AnimateAction { } } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum AnimateSwingSource { + None = 1, + Build = 2, + Mine = 3, + Interact = 4, + Attack = 5, + UseItem = 6, + ThrowItem = 7, + DropItem = 8, + Event = 9, +} + +impl FromStr for AnimateSwingSource { + type Err = Error; + + fn from_str(s: &str) -> Result { + match s { + "none" => Ok(Self::None), + "build" => Ok(Self::Build), + "mine" => Ok(Self::Mine), + "interact" => Ok(Self::Interact), + "attack" => Ok(Self::Attack), + "useitem" => Ok(Self::UseItem), + "throwitem" => Ok(Self::ThrowItem), + "dropitem" => Ok(Self::DropItem), + "event" => Ok(Self::Event), + _ => Err(Error::other(format!("Unknown swing source: {s}"))), + } + } +} + +impl AnimateSwingSource { + #[must_use] + pub const fn as_str(&self) -> &'static str { + match self { + Self::None => "none", + Self::Build => "build", + Self::Mine => "mine", + Self::Interact => "interact", + Self::Attack => "attack", + Self::UseItem => "useitem", + Self::ThrowItem => "throwitem", + Self::DropItem => "dropitem", + Self::Event => "event", + } + } +} + +#[derive(Debug)] +#[packet(44)] +pub struct SAnimate { + pub action: AnimateAction, + pub runtime_entity_id: VarULong, + pub data: f32, + pub swing_source: Option, +} + impl PacketRead for SAnimate { fn read(reader: &mut R) -> Result { let action = AnimateAction::read(reader)?; let runtime_entity_id = VarULong::read(reader)?; - let boat_rowing_time = - if action == AnimateAction::RowRight || action == AnimateAction::RowLeft { - Some(f32::read(reader)?) - } else { - None - }; - let _swing_source = bool::read(reader)?; + let data = f32::read(reader)?; + + let swing_source_str = Option::::read(reader)?; + let swing_source = match swing_source_str { + Some(s) => Some(AnimateSwingSource::from_str(&s)?), + None => None, + }; + Ok(Self { action, runtime_entity_id, - boat_rowing_time, + data, + swing_source, }) } } @@ -71,11 +122,11 @@ impl PacketWrite for SAnimate { fn write(&self, writer: &mut W) -> Result<(), Error> { self.action.write(writer)?; self.runtime_entity_id.write(writer)?; - if let Some(rowing_time) = self.boat_rowing_time { - rowing_time.write(writer)?; - } - // swingSource - false.write(writer)?; + self.data.write(writer)?; + + let swing_source_str: Option = self.swing_source.map(|s| s.as_str().to_string()); + swing_source_str.write(writer)?; + Ok(()) } } diff --git a/pumpkin-protocol/src/bedrock/server/interaction.rs b/pumpkin-protocol/src/bedrock/server/interaction.rs index 0ec3907c6..b3b7fd1fe 100644 --- a/pumpkin-protocol/src/bedrock/server/interaction.rs +++ b/pumpkin-protocol/src/bedrock/server/interaction.rs @@ -18,6 +18,8 @@ pub struct SInteraction { #[repr(i8)] pub enum Action { Invalid = 0, + Interact = 1, + Attack = 2, StopRiding = 3, InteractUpdate = 4, NpcOpen = 5, @@ -31,6 +33,8 @@ impl PacketRead for Action { let this = match byte[0] { 0 => Self::Invalid, + 1 => Self::Interact, + 2 => Self::Attack, 3 => Self::StopRiding, 4 => Self::InteractUpdate, 5 => Self::NpcOpen, diff --git a/pumpkin-protocol/src/bedrock/server/player_auth_input.rs b/pumpkin-protocol/src/bedrock/server/player_auth_input.rs index a2ad81263..0c1d8c540 100644 --- a/pumpkin-protocol/src/bedrock/server/player_auth_input.rs +++ b/pumpkin-protocol/src/bedrock/server/player_auth_input.rs @@ -199,7 +199,7 @@ pub enum InputData { StartFlying = 42, StopFlying = 43, ClientAckServerData = 44, - ClientPredictedVehicle = 45, // Renamed to match Go + ClientPredictedVehicle = 45, PaddlingLeft = 46, PaddlingRight = 47, BlockBreakingDelayEnabled = 48, diff --git a/pumpkin-protocol/src/codec/var_long.rs b/pumpkin-protocol/src/codec/var_long.rs index 0aa8bc72e..ef8811687 100644 --- a/pumpkin-protocol/src/codec/var_long.rs +++ b/pumpkin-protocol/src/codec/var_long.rs @@ -20,7 +20,7 @@ pub type VarLongType = i64; /** * A variable-length long type used by the Minecraft network protocol. */ -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Default)] pub struct VarLong(pub VarLongType); impl VarLong { diff --git a/pumpkin-protocol/src/codec/var_ulong.rs b/pumpkin-protocol/src/codec/var_ulong.rs index 1a1ccbddf..4bb8ec171 100644 --- a/pumpkin-protocol/src/codec/var_ulong.rs +++ b/pumpkin-protocol/src/codec/var_ulong.rs @@ -20,7 +20,7 @@ pub type VarULongType = u64; /** * A variable-length long type used by the Minecraft network protocol. */ -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub struct VarULong(pub VarULongType); impl VarULong { diff --git a/pumpkin/Cargo.toml b/pumpkin/Cargo.toml index 90206191a..b994cdf44 100644 --- a/pumpkin/Cargo.toml +++ b/pumpkin/Cargo.toml @@ -109,6 +109,7 @@ postcard = { workspace = true } tracing-serde-structured = { workspace = true } bitflags = { workspace = true } +image = { version = "0.25.10", default-features = false, features = ["png"] } [dev-dependencies] tempfile.workspace = true diff --git a/pumpkin/src/entity/ai/goal/melee_attack.rs b/pumpkin/src/entity/ai/goal/melee_attack.rs index 68f931ecb..5168d55e3 100644 --- a/pumpkin/src/entity/ai/goal/melee_attack.rs +++ b/pumpkin/src/entity/ai/goal/melee_attack.rs @@ -197,7 +197,7 @@ impl Goal for MeleeAttackGoal { .await { self.cooldown = self.get_max_cooldown(); - mob.get_mob_entity().living_entity.swing_hand(); + mob.get_mob_entity().living_entity.swing_hand().await; mob.get_mob_entity().try_attack(mob, target.as_ref()).await; } }) diff --git a/pumpkin/src/entity/living.rs b/pumpkin/src/entity/living.rs index a9280a595..a4a2d4faf 100644 --- a/pumpkin/src/entity/living.rs +++ b/pumpkin/src/entity/living.rs @@ -47,8 +47,7 @@ use pumpkin_nbt::compound::NbtCompound; use pumpkin_nbt::tag::NbtTag; use pumpkin_protocol::codec::var_int::VarInt; use pumpkin_protocol::java::client::play::{ - Animation, CEntityAnimation, CEntityStatus, CHurtAnimation, CSetPlayerInventory, - CTakeItemEntity, CUpdateMobEffect, + CEntityStatus, CHurtAnimation, CSetPlayerInventory, CTakeItemEntity, CUpdateMobEffect, }; use pumpkin_protocol::{ codec::item_stack_seralizer::ItemStackSerializer, @@ -715,15 +714,22 @@ impl LivingEntity { } } - pub fn swing_hand(&self) { - // TODO: radius - self.entity - .world - .load() - .broadcast_packet_all(&CEntityAnimation::new( - self.entity_id().into(), - Animation::SwingMainArm, - )); + pub async fn swing_hand(&self) { + let world = self.entity.world.load(); + let entity_id = self.entity_id(); + + let je_packet = pumpkin_protocol::java::client::play::CEntityAnimation::new( + entity_id.into(), + pumpkin_protocol::java::client::play::Animation::SwingMainArm, + ); + let be_packet = pumpkin_protocol::bedrock::server::animate::SAnimate { + action: pumpkin_protocol::bedrock::server::animate::AnimateAction::SwingArm, + runtime_entity_id: pumpkin_protocol::codec::var_ulong::VarULong(entity_id as u64), + data: 0.0, + swing_source: None, + }; + + world.broadcast_editioned(&je_packet, &be_packet).await; } async fn tick_movement<'a>(&'a self, server: &'a Server, caller: &'a Arc) { diff --git a/pumpkin/src/entity/mod.rs b/pumpkin/src/entity/mod.rs index 602de993f..44eace0b7 100644 --- a/pumpkin/src/entity/mod.rs +++ b/pumpkin/src/entity/mod.rs @@ -35,9 +35,18 @@ use pumpkin_nbt::{compound::NbtCompound, tag::NbtTag}; use pumpkin_protocol::java::client::play::{CUpdateEntityPos, CUpdateEntityPosRot}; use pumpkin_protocol::{ PositionFlag, - bedrock::client::set_actor_data::{ - CSetActorData, EntityMetadata, MetadataValue, PropertySyncData, entity_data_flag, - entity_data_key, + bedrock::client::{ + move_actor_delta::{ + CMoveActorDelta, MOVE_ACTOR_DELTA_FLAG_HAS_HEAD_YAW, MOVE_ACTOR_DELTA_FLAG_HAS_PITCH, + MOVE_ACTOR_DELTA_FLAG_HAS_X, MOVE_ACTOR_DELTA_FLAG_HAS_Y, + MOVE_ACTOR_DELTA_FLAG_HAS_YAW, MOVE_ACTOR_DELTA_FLAG_HAS_Z, + MOVE_ACTOR_DELTA_FLAG_ON_GROUND, + }, + move_player::CMovePlayer, + set_actor_data::{ + CSetActorData, EntityMetadata, MetadataValue, PropertySyncData, entity_data_flag, + entity_data_key, + }, }, codec::var_int::VarInt, codec::var_ulong::VarULong, @@ -1129,6 +1138,7 @@ impl Entity { suffocating } + #[expect(clippy::too_many_lines)] pub fn send_pos_rot(&self) { let old = self.last_sent_pos.load(); let new = self.pos.load(); @@ -1161,35 +1171,150 @@ impl Entity { // Dynamically pick the most efficient packet if pos_changed && rot_changed { - self.world.load().broadcast_to_chunk( - chunk_pos, - &CUpdateEntityPosRot::new( - self.entity_id.into(), - Vector3::new(converted.x, converted.y, converted.z), - yaw, - pitch, - self.on_ground.load(Relaxed), - ), + let je_packet = CUpdateEntityPosRot::new( + self.entity_id.into(), + Vector3::new(converted.x, converted.y, converted.z), + yaw, + pitch, + self.on_ground.load(Relaxed), ); + if self.entity_type == &EntityType::PLAYER { + self.world.load().broadcast_to_chunk_editioned_sync( + chunk_pos, + &je_packet, + &CMovePlayer::new( + VarULong(self.entity_id as u64), + Vector3::new(new.x as f32, new.y as f32, new.z as f32), + self.pitch.load(), + self.yaw.load(), + self.yaw.load(), + CMovePlayer::MODE_NORMAL, + self.on_ground.load(Relaxed), + VarULong(0), + 0, + 0, + VarULong(0), + ), + ); + } else { + let mut flags = MOVE_ACTOR_DELTA_FLAG_HAS_X + | MOVE_ACTOR_DELTA_FLAG_HAS_Y + | MOVE_ACTOR_DELTA_FLAG_HAS_Z + | MOVE_ACTOR_DELTA_FLAG_HAS_PITCH + | MOVE_ACTOR_DELTA_FLAG_HAS_YAW + | MOVE_ACTOR_DELTA_FLAG_HAS_HEAD_YAW; + if self.on_ground.load(Relaxed) { + flags |= MOVE_ACTOR_DELTA_FLAG_ON_GROUND; + } + self.world.load().broadcast_to_chunk_editioned_sync( + chunk_pos, + &je_packet, + &CMoveActorDelta::new( + VarULong(self.entity_id as u64), + flags, + new.x as f32, + new.y as f32, + new.z as f32, + pitch, + yaw, + yaw, + ), + ); + } } else if pos_changed { - self.world.load().broadcast_to_chunk( - chunk_pos, - &CUpdateEntityPos::new( - self.entity_id.into(), - Vector3::new(converted.x, converted.y, converted.z), - self.on_ground.load(Relaxed), - ), + let je_packet = CUpdateEntityPos::new( + self.entity_id.into(), + Vector3::new(converted.x, converted.y, converted.z), + self.on_ground.load(Relaxed), ); + if self.entity_type == &EntityType::PLAYER { + self.world.load().broadcast_to_chunk_editioned_sync( + chunk_pos, + &je_packet, + &CMovePlayer::new( + VarULong(self.entity_id as u64), + Vector3::new(new.x as f32, new.y as f32, new.z as f32), + self.pitch.load(), + self.yaw.load(), + self.yaw.load(), + CMovePlayer::MODE_NORMAL, + self.on_ground.load(Relaxed), + VarULong(0), + 0, + 0, + VarULong(0), + ), + ); + } else { + let mut flags = MOVE_ACTOR_DELTA_FLAG_HAS_X + | MOVE_ACTOR_DELTA_FLAG_HAS_Y + | MOVE_ACTOR_DELTA_FLAG_HAS_Z; + if self.on_ground.load(Relaxed) { + flags |= MOVE_ACTOR_DELTA_FLAG_ON_GROUND; + } + + self.world.load().broadcast_to_chunk_editioned_sync( + chunk_pos, + &je_packet, + &CMoveActorDelta::new( + VarULong(self.entity_id as u64), + flags, + new.x as f32, + new.y as f32, + new.z as f32, + 0, + 0, + 0, + ), + ); + } } else if rot_changed { - self.world.load().broadcast_to_chunk( - chunk_pos, - &CUpdateEntityRot::new( - self.entity_id.into(), - yaw, - pitch, - self.on_ground.load(Relaxed), - ), + let je_packet = CUpdateEntityRot::new( + self.entity_id.into(), + yaw, + pitch, + self.on_ground.load(Relaxed), ); + if self.entity_type == &EntityType::PLAYER { + self.world.load().broadcast_to_chunk_editioned_sync( + chunk_pos, + &je_packet, + &CMovePlayer::new( + VarULong(self.entity_id as u64), + Vector3::new(new.x as f32, new.y as f32, new.z as f32), + self.pitch.load(), + self.yaw.load(), + self.yaw.load(), + CMovePlayer::MODE_ROTATION, + self.on_ground.load(Relaxed), + VarULong(0), + 0, + 0, + VarULong(0), + ), + ); + } else { + let mut flags = MOVE_ACTOR_DELTA_FLAG_HAS_PITCH + | MOVE_ACTOR_DELTA_FLAG_HAS_YAW + | MOVE_ACTOR_DELTA_FLAG_HAS_HEAD_YAW; + if self.on_ground.load(Relaxed) { + flags |= MOVE_ACTOR_DELTA_FLAG_ON_GROUND; + } + self.world.load().broadcast_to_chunk_editioned_sync( + chunk_pos, + &je_packet, + &CMoveActorDelta::new( + VarULong(self.entity_id as u64), + flags, + new.x as f32, + new.y as f32, + new.z as f32, + pitch, + yaw, + yaw, + ), + ); + } } self.send_head_rot(yaw); } @@ -1220,14 +1345,53 @@ impl Entity { self.last_sent_pos.store(new); - self.world.load().broadcast_to_chunk( - chunk_pos, - &CUpdateEntityPos::new( - self.entity_id.into(), - Vector3::new(converted.x, converted.y, converted.z), - self.on_ground.load(Relaxed), - ), + let je_packet = CUpdateEntityPos::new( + self.entity_id.into(), + Vector3::new(converted.x, converted.y, converted.z), + self.on_ground.load(Relaxed), ); + + if self.entity_type == &EntityType::PLAYER { + self.world.load().broadcast_to_chunk_editioned_sync( + chunk_pos, + &je_packet, + &CMovePlayer::new( + VarULong(self.entity_id as u64), + Vector3::new(new.x as f32, new.y as f32, new.z as f32), + self.pitch.load(), + self.yaw.load(), + self.yaw.load(), + CMovePlayer::MODE_NORMAL, + self.on_ground.load(Relaxed), + VarULong(0), + 0, + 0, + VarULong(0), + ), + ); + } else { + let mut flags = MOVE_ACTOR_DELTA_FLAG_HAS_X + | MOVE_ACTOR_DELTA_FLAG_HAS_Y + | MOVE_ACTOR_DELTA_FLAG_HAS_Z; + if self.on_ground.load(Relaxed) { + flags |= MOVE_ACTOR_DELTA_FLAG_ON_GROUND; + } + + self.world.load().broadcast_to_chunk_editioned_sync( + chunk_pos, + &je_packet, + &CMoveActorDelta::new( + VarULong(self.entity_id as u64), + flags, + new.x as f32, + new.y as f32, + new.z as f32, + 0, + 0, + 0, + ), + ); + } } // updateWaterState() in yarn diff --git a/pumpkin/src/entity/player.rs b/pumpkin/src/entity/player.rs index c4623bf65..c2ff0fd9b 100644 --- a/pumpkin/src/entity/player.rs +++ b/pumpkin/src/entity/player.rs @@ -15,12 +15,11 @@ use pumpkin_data::dimension::Dimension; use pumpkin_data::meta_data_type::MetaDataType; use pumpkin_data::tracked_data::TrackedData; use pumpkin_inventory::player::ender_chest_inventory::EnderChestInventory; +use pumpkin_protocol::bedrock::client::AbilityLayer; use pumpkin_protocol::bedrock::client::level_chunk::CLevelChunk; use pumpkin_protocol::bedrock::client::play_status::CPlayStatus; use pumpkin_protocol::bedrock::client::set_time::CSetTime; -use pumpkin_protocol::bedrock::client::update_abilities::{ - Ability, AbilityLayer, CUpdateAbilities, -}; +use pumpkin_protocol::bedrock::client::update_abilities::{Ability, CUpdateAbilities}; use pumpkin_protocol::bedrock::frame_set::FrameSet; use pumpkin_protocol::bedrock::server::text::SText; use pumpkin_protocol::codec::item_stack_seralizer::ItemStackSerializer; @@ -493,9 +492,66 @@ pub struct Player { pub tab_list_listed: AtomicBool, pub enchantment_seed: AtomicI32, pub fishing_bobber: AtomicI32, + pub bedrock_skin: arc_swap::ArcSwap, +} + +use base64::prelude::*; +use pumpkin_protocol::Property; +use serde::Deserialize; +use std::io::Read; + +#[derive(Deserialize)] +struct TexturesProperty { + textures: Textures, +} + +#[derive(Deserialize)] +struct Textures { + #[serde(rename = "SKIN")] + skin: Option, +} + +#[derive(Deserialize)] +struct SkinTexture { + url: String, } impl Player { + #[must_use] + pub fn fetch_skin(properties: &[Property]) -> Option { + let textures_prop = properties.iter().find(|p| &*p.name == "textures")?; + let decoded = BASE64_STANDARD + .decode(textures_prop.value.as_bytes()) + .ok()?; + let textures: TexturesProperty = serde_json::from_slice(&decoded).ok()?; + let url = textures.textures.skin?.url; + + let resp = ureq::get(&url).call().ok()?; + let mut buf = Vec::new(); + resp.into_body().into_reader().read_to_end(&mut buf).ok()?; + let img = image::load_from_memory(&buf).ok()?; + + let width = img.width(); + let mut height = img.height(); + + if width != 64 || (height != 32 && height != 64) { + return None; + } + + let mut rgba = img.into_rgba8().into_raw(); + + if height == 32 { + rgba.resize(64 * 64 * 4, 0); + height = 64; + } + + let mut skin = pumpkin_protocol::bedrock::client::Skin::steve(); + skin.image_width = width; + skin.image_height = height; + skin.skin_data = rgba; + Some(skin) + } + #[expect(clippy::too_many_lines)] pub async fn new( client: ClientPlatform, @@ -544,6 +600,14 @@ impl Player { let mut abilities = Abilities::default(); abilities.set_for_gamemode(gamemode); + let properties = gameprofile.properties.load().clone(); + let bedrock_skin = tokio::task::spawn_blocking(move || { + Self::fetch_skin(&properties) + .unwrap_or_else(pumpkin_protocol::bedrock::client::Skin::steve) + }) + .await + .unwrap_or_else(|_| pumpkin_protocol::bedrock::client::Skin::steve()); + Self { living_entity, config: ArcSwap::new(Arc::new(config)), @@ -633,6 +697,7 @@ impl Player { tab_list_latency: AtomicI32::new(0), tab_list_listed: AtomicBool::new(false), fishing_bobber: AtomicI32::new(-1), + bedrock_skin: ArcSwap::new(Arc::new(bedrock_skin)), } } @@ -3545,21 +3610,33 @@ impl Player { } /// Swing the hand of the player - pub fn swing_hand(&self, hand: Hand, all: bool) { + pub async fn swing_hand(&self, hand: Hand, all: bool) { let world = self.world(); - let entity_id = VarInt(self.entity_id()); - let chunk_pos = self.living_entity.entity.chunk_pos.load(); + let entity_id = self.entity_id(); let animation = match hand { - Hand::Left => Animation::SwingMainArm, - Hand::Right => Animation::SwingOffhand, + Hand::Right => Animation::SwingMainArm, + Hand::Left => Animation::SwingOffhand, + }; + + let je_packet = pumpkin_protocol::java::client::play::CEntityAnimation::new( + VarInt(entity_id), + animation, + ); + + let be_packet = pumpkin_protocol::bedrock::server::animate::SAnimate { + action: pumpkin_protocol::bedrock::server::animate::AnimateAction::SwingArm, + runtime_entity_id: pumpkin_protocol::codec::var_ulong::VarULong(entity_id as u64), + data: 0.0, + swing_source: None, }; - let packet = CEntityAnimation::new(entity_id, animation); if all { - world.broadcast_to_chunk(chunk_pos, &packet); + world.broadcast_editioned(&je_packet, &be_packet).await; } else { - world.broadcast_to_chunk_except(chunk_pos, &[self.get_entity().entity_uuid], &packet); + world + .broadcast_packet_except_editioned(&[self.gameprofile.id], &je_packet, &be_packet) + .await; } } diff --git a/pumpkin/src/lib.rs b/pumpkin/src/lib.rs index 8d7ad77cb..f800c58b8 100644 --- a/pumpkin/src/lib.rs +++ b/pumpkin/src/lib.rs @@ -5,8 +5,8 @@ use crate::crash::CrashReport; use crate::data::VanillaData; use crate::logging::{GzipRollingLogger, PumpkinCommandCompleter, ReadlineLogWrapper}; use crate::net::bedrock::BedrockClient; -use crate::net::java::{JavaClient, PacketHandlerResult}; -use crate::net::{ClientPlatform, DisconnectReason}; +use crate::net::java::JavaClient; +use crate::net::{ClientPlatform, DisconnectReason, PacketHandlerResult}; use crate::net::{lan_broadcast::LANBroadcast, query, rcon::RCONServer}; use crate::server::{Server, ticker::Ticker}; use plugin::server::server_command::ServerCommandEvent; @@ -418,6 +418,7 @@ impl PumpkinServer { } } + #[allow(clippy::too_many_lines)] pub async fn unified_listener_task( &self, master_client_id_counter: &mut u64, @@ -501,7 +502,9 @@ impl PumpkinServer { let be_clients = bedrock_clients.clone(); let mut clients_guard = bedrock_clients.lock().await; + let mut is_new = false; let client = clients_guard.entry(client_addr).or_insert_with(|| { + is_new = true; *master_client_id_counter += 1; let new_client = Arc::new(BedrockClient::new( @@ -514,6 +517,39 @@ impl PumpkinServer { new_client }).clone(); + if is_new { + let server_clone = self.server.clone(); + let client_clone = client.clone(); + tasks.spawn(async move { + let login_result = client_clone.handle_login_sequence(&server_clone).await; + + match login_result { + PacketHandlerResult::Stop => { + client_clone.close().await; + } + PacketHandlerResult::ReadyToPlay(profile, config) => { + if let Some((player, _world)) = server_clone + .add_player(ClientPlatform::Bedrock(client_clone.clone()), profile, Some(config)) + .await + { + *client_clone.player.lock().await = Some(player.clone()); + + client_clone.progress_player_packets(&player, &server_clone).await; + + client_clone.close().await; + player.remove().await; + server_clone.remove_player(&player).await; + if let Err(e) = server_clone.player_data_storage + .handle_player_leave(&player) + .await { + error!("Failed to save player data on disconnect: {e}"); + } + } + } + } + }); + } + let packet_bytes = udp_buf[..len].to_vec(); let server = self.server.clone(); diff --git a/pumpkin/src/net/bedrock/login.rs b/pumpkin/src/net/bedrock/login.rs index f1bff8df1..c4d82440b 100644 --- a/pumpkin/src/net/bedrock/login.rs +++ b/pumpkin/src/net/bedrock/login.rs @@ -1,5 +1,7 @@ use crate::{ - net::{ClientPlatform, DisconnectReason, GameProfile, PlayerConfig, bedrock::BedrockClient}, + net::{ + DisconnectReason, GameProfile, PacketHandlerResult, PlayerConfig, bedrock::BedrockClient, + }, server::Server, }; use arc_swap::ArcSwap; @@ -23,7 +25,7 @@ use serde::{Deserialize, de::Error}; use serde_repr::Deserialize_repr; use std::sync::Arc; use thiserror::Error; -use tracing::{debug, warn}; +use tracing::debug; use uuid::Uuid; #[derive(Debug, Error)] @@ -117,27 +119,19 @@ impl BedrockClient { self.set_compression(compression).await; } - pub async fn handle_login(self: &Arc, packet: SLogin, server: &Server) -> Option<()> { - match self.try_handle_login(packet, server).await { - Ok(()) => Some(()), - Err(error) => { - warn!("Bedrock login failed: {error}"); - let message = match error { - LoginError::InvalidUsername => "Your username is invalid.".to_string(), - _ => "Failed to log in. The data sent by your client was invalid.".to_string(), - }; - self.kick(DisconnectReason::LoginPacketNoRequest, message) - .await; - None - } - } + pub async fn handle_login( + self: &Arc, + packet: SLogin, + server: &Server, + ) -> Result { + self.try_handle_login(packet, server).await } pub async fn try_handle_login( self: &Arc, packet: SLogin, server: &Server, - ) -> Result<(), LoginError> { + ) -> Result { let auth_payload: AuthPayload = serde_json::from_slice(&packet.jwt)?; let player_data = if server.basic_config.online_mode { match auth_payload.authentication_type { @@ -222,26 +216,15 @@ impl BedrockClient { self.send_frame_set(frame_set, 0x84).await; - if let Some((player, _world)) = server - .add_player(ClientPlatform::Bedrock(self.clone()), profile, None) - .await - { - // TODO: kinda sad we don't use more of client_data, we should store it somewhere, at least for plugin devs - let new_config = PlayerConfig { - locale: client_data.language_code.clone(), - ..Default::default() - }; + let new_config = PlayerConfig { + locale: client_data.language_code.clone(), + ..Default::default() + }; - player.config.store(std::sync::Arc::new(new_config)); + self.client_data + .store(std::sync::Arc::new(Some(std::sync::Arc::new(client_data)))); - self.client_data - .store(std::sync::Arc::new(Some(std::sync::Arc::new(client_data)))); - - // player spawn happens after resource packs are resolved - *self.player.lock().await = Some(player); - } - - Ok(()) + Ok(PacketHandlerResult::ReadyToPlay(profile, new_config)) } pub async fn handle_resource_pack_response( diff --git a/pumpkin/src/net/bedrock/mod.rs b/pumpkin/src/net/bedrock/mod.rs index a2b4202ea..647daf612 100644 --- a/pumpkin/src/net/bedrock/mod.rs +++ b/pumpkin/src/net/bedrock/mod.rs @@ -57,8 +57,8 @@ use pumpkin_protocol::{ use std::net::SocketAddr; use tokio::{ net::UdpSocket, - sync::Mutex, sync::mpsc::{Receiver, Sender}, + sync::{Mutex, oneshot}, task::JoinHandle, }; use tokio_util::{sync::CancellationToken, task::TaskTracker}; @@ -67,11 +67,36 @@ pub mod connection; pub mod login; pub mod open_connection; pub mod unconnected; -use crate::{entity::player::Player, net::DisconnectReason, server::Server}; +use crate::{ + entity::player::Player, + net::{DisconnectReason, PacketHandlerResult}, + server::Server, +}; use arc_swap::ArcSwap; use pumpkin_protocol::bedrock::server::login::ClientData; use pumpkin_util::version::BedrockMinecraftVersion; +pub struct OutgoingPacket { + pub data: Bytes, + pub completion: Option>, +} + +impl OutgoingPacket { + pub const fn normal(data: Bytes) -> Self { + Self { + data, + completion: None, + } + } + + pub const fn priority(data: Bytes, completion: oneshot::Sender<()>) -> Self { + Self { + data, + completion: Some(completion), + } + } +} + pub struct BedrockClient { socket: Arc, /// The client's IP address. @@ -84,9 +109,12 @@ pub struct BedrockClient { pub be_clients: Arc>>>, tasks: TaskTracker, - outgoing_packet_queue_send: Sender, + outgoing_packet_queue_send: Sender, /// A queue of serialized packets to send to the network - outgoing_packet_queue_recv: Mutex>>, + outgoing_packet_queue_recv: Mutex>>, + + outgoing_packet_priority_send: Sender, + outgoing_packet_priority_recv: Mutex>>, /// The packet encoder for outgoing packets. network_writer: Arc>, @@ -114,6 +142,8 @@ pub struct BedrockClient { expected_order_index: Mutex>, highest_sequence_index: Mutex>, ordered_queues: Mutex>>, + incoming_game_packet_send: Sender, + incoming_game_packet_recv: Mutex>>, } impl BedrockClient { @@ -124,6 +154,8 @@ impl BedrockClient { be_clients: Arc>>>, ) -> Self { let (send, recv) = tokio::sync::mpsc::channel(128); + let (priority_send, priority_recv) = tokio::sync::mpsc::channel(128); + let (incoming_send, incoming_recv) = tokio::sync::mpsc::channel(128); Self { socket, player: Mutex::new(None), @@ -136,6 +168,8 @@ impl BedrockClient { tasks: TaskTracker::new(), outgoing_packet_queue_send: send, outgoing_packet_queue_recv: Mutex::new(Some(recv)), + outgoing_packet_priority_send: priority_send, + outgoing_packet_priority_recv: Mutex::new(Some(priority_recv)), _use_frame_sets: AtomicBool::new(false), output_sequence_number: AtomicU32::new(0), output_reliable_number: AtomicU32::new(0), @@ -153,6 +187,17 @@ impl BedrockClient { highest_sequence_index: Mutex::new(HashMap::new()), ordered_queues: Mutex::new(HashMap::new()), //input_sequence_number: AtomicU32::new(0), + incoming_game_packet_send: incoming_send, + incoming_game_packet_recv: Mutex::new(Some(incoming_recv)), + } + } + + pub async fn get_packet(&self) -> Option { + let mut guard = self.incoming_game_packet_recv.lock().await; + let recv = guard.as_mut()?; + tokio::select! { + () = self.await_close_interrupt() => None, + packet = recv.recv() => packet, } } @@ -165,16 +210,25 @@ impl BedrockClient { .take() .expect("Outgoing packet receiver was already taken") }; + let mut priority_packet_receiver = { + let mut guard = client.outgoing_packet_priority_recv.lock().await; + guard + .take() + .expect("Outgoing packet receiver was already taken") + }; let mut interval = tokio::time::interval(std::time::Duration::from_millis(100)); while !client.close_token.is_cancelled() { - tokio::select! { + let recv_result = tokio::select! { + biased; + () = client.close_token.cancelled() => None, + res = priority_packet_receiver.recv() => res, _ = interval.tick() => { // Check for timeout (10 seconds) if client.last_seen.load().elapsed() > std::time::Duration::from_secs(10) { debug!("Bedrock client {} timed out", client.address); client.close().await; - break; + return; } // Flush ACKs @@ -207,23 +261,21 @@ impl BedrockClient { warn!("Failed to resend packet for sequence {}: {}", seq, err); } } + continue; } - recv_result = packet_receiver.recv() => { - let Some(packet_data) = recv_result else { - break; - }; + res = packet_receiver.recv() => res, + }; - if let Err(err) = client.network_writer - .lock() - .await - .write_packet(&packet_data, client.address, &client.socket) - .await - && !client.close_token.is_cancelled() { - warn!("Failed to send packet to client: {err}",); - client.close_token.cancel(); - break; - } - } + let Some(packet) = recv_result else { + break; + }; + + client + .send_framed_packet_data(packet.data.to_vec(), RakReliability::ReliableOrdered) + .await; + + if let Some(completion) = packet.completion { + let _ = completion.send(()); } } }); @@ -259,6 +311,43 @@ impl BedrockClient { self.close().await; } + pub async fn enqueue_packet(&self, packet: &P) { + let mut packet_buf = Vec::new(); + match self.write_game_packet(packet, &mut packet_buf).await { + Ok(()) => self.enqueue_packet_data(packet_buf.into()).await, + Err(err) => error!("Failed to write game packet: {err}"), + } + } + + pub fn try_enqueue_packet(&self, packet: &P) { + let mut packet_buf = Vec::new(); + let mut packet_payload = Vec::new(); + if let Err(err) = packet.write_packet(&mut packet_payload) { + error!("Failed to write packet for try_enqueue_packet: {err}"); + return; + } + + { + let Ok(mut network_writer) = self.network_writer.try_lock() else { + debug!("Failed to lock network writer for try_enqueue_packet"); + return; + }; + + if let Err(err) = network_writer.write_game_packet( + P::PACKET_ID as u16, + SubClient::Main, + SubClient::Main, + &packet_payload, + &mut packet_buf, + ) { + error!("Failed to write game packet for try_enqueue_packet: {err}"); + return; + } + } + + self.try_enqueue_packet_data(packet_buf.into()); + } + /// Queues a clientbound packet to be sent to the connected client. Queued chunks are sent /// in-order to the client /// @@ -266,7 +355,11 @@ impl BedrockClient { /// /// * `packet`: A reference to a packet object implementing the `ClientPacket` trait. pub async fn enqueue_packet_data(&self, packet_data: Bytes) { - if let Err(err) = self.outgoing_packet_queue_send.send(packet_data).await { + if let Err(err) = self + .outgoing_packet_queue_send + .send(OutgoingPacket::normal(packet_data)) + .await + { // This is expected to fail if we are closed if !self.is_closed() { error!("Failed to add packet to the outgoing packet queue for client: {err}"); @@ -275,7 +368,10 @@ impl BedrockClient { } pub fn try_enqueue_packet_data(&self, packet_data: Bytes) { - if let Err(err) = self.outgoing_packet_queue_send.try_send(packet_data) { + if let Err(err) = self + .outgoing_packet_queue_send + .try_send(OutgoingPacket::normal(packet_data)) + { match err { tokio::sync::mpsc::error::TrySendError::Full(_) => { debug!( @@ -309,8 +405,8 @@ impl BedrockClient { let mut packet_payload = Vec::new(); packet.write_packet(&mut packet_payload)?; - // TODO - self.network_writer.lock().await.write_game_packet( + let mut encoder = self.network_writer.lock().await; + encoder.write_game_packet( P::PACKET_ID as u16, SubClient::Main, SubClient::Main, @@ -337,8 +433,18 @@ impl BedrockClient { let mut packet_buf = Vec::new(); match self.write_game_packet(packet, &mut packet_buf).await { Ok(()) => { - self.send_framed_packet_data(packet_buf, RakReliability::ReliableOrdered) - .await; + let (tx, rx) = oneshot::channel(); + if let Err(err) = self + .outgoing_packet_priority_send + .send(OutgoingPacket::priority(packet_buf.into(), tx)) + .await + { + if !self.is_closed() { + error!("Failed to add priority packet to the outgoing packet queue: {err}"); + } + } else { + let _ = rx.await; + } } Err(err) => error!("Failed to write game packet: {err}"), } @@ -710,48 +816,70 @@ impl BedrockClient { } async fn handle_game_packet( - self: &Arc, - server: &Arc, + &self, + _server: &Arc, packet: RawPacket, ) -> Result<(), Error> { - let packet_id = packet.id; - let payload = &mut Cursor::new(&packet.payload); - let result = match packet.id { - SRequestNetworkSettings::PACKET_ID => { - self.handle_request_network_settings( - SRequestNetworkSettings::read(payload)?, - server, - ) - .await; - Ok(()) - } - SLogin::PACKET_ID => { - self.handle_login(SLogin::read(payload)?, server).await; - Ok(()) - } - SClientCacheStatus::PACKET_ID => { - // TODO - Ok(()) - } - SResourcePackResponse::PACKET_ID => { - self.handle_resource_pack_response(SResourcePackResponse::read(payload)?, server) - .await; - Ok(()) - } - _ => { - let player_lock = self.player.lock().await; - if let Some(player) = player_lock.as_ref() { - self.handle_play_packet(player, server, packet).await - } else { + if let Err(err) = self.incoming_game_packet_send.send(packet).await { + debug!("Failed to send game packet to session task: {err}"); + } + Ok(()) + } + + pub async fn handle_login_sequence( + self: &Arc, + server: &Arc, + ) -> PacketHandlerResult { + while let Some(packet) = self.get_packet().await { + let payload = &mut Cursor::new(&packet.payload); + match packet.id { + SRequestNetworkSettings::PACKET_ID => { + let packet = match SRequestNetworkSettings::read(payload) { + Ok(p) => p, + Err(err) => { + error!("Failed to read SRequestNetworkSettings: {err}"); + continue; + } + }; + self.handle_request_network_settings(packet, server).await; + } + SLogin::PACKET_ID => { + let packet = match SLogin::read(payload) { + Ok(p) => p, + Err(err) => { + error!("Failed to read SLogin: {err}"); + continue; + } + }; + match self.handle_login(packet, server).await { + Ok(result) => return result, + Err(err) => { + warn!("Bedrock login failed: {err}"); + return PacketHandlerResult::Stop; + } + } + } + _ => { debug!( - "Received game packet {} before player was initialized", + "Received unexpected game packet {} during login sequence", packet.id ); - Ok(()) } } - }; - result.map_err(|e| Error::new(e.kind(), format!("Game packet {packet_id}: {e}"))) + } + PacketHandlerResult::Stop + } + + pub async fn progress_player_packets( + self: &Arc, + player: &Arc, + server: &Arc, + ) { + while let Some(packet) = self.get_packet().await { + if let Err(err) = self.handle_play_packet(player, server, packet).await { + error!("Failed to handle Bedrock play packet: {err}"); + } + } } pub async fn handle_play_packet( @@ -760,8 +888,16 @@ impl BedrockClient { server: &Arc, packet: RawPacket, ) -> Result<(), Error> { - let reader = &mut &packet.payload[..]; + let payload = &packet.payload[..]; + let reader = &mut &payload[..]; match packet.id { + SClientCacheStatus::PACKET_ID => { + // TODO + } + SResourcePackResponse::PACKET_ID => { + self.handle_resource_pack_response(SResourcePackResponse::read(reader)?, server) + .await; + } SPlayerAuthInput::PACKET_ID => { self.handle_player_auth_input(player, SPlayerAuthInput::read(reader)?, server) .await; @@ -797,7 +933,7 @@ impl BedrockClient { .await; } SAnimate::PACKET_ID => { - self.handle_animate(player, server, &SAnimate::read(reader)?); + self.handle_animate(player, server, &SAnimate::read(reader)?).await; } pumpkin_protocol::bedrock::server::modal_form_response::SModalFormResponse::PACKET_ID => { self.handle_modal_form_response( diff --git a/pumpkin/src/net/bedrock/play.rs b/pumpkin/src/net/bedrock/play.rs index 9cd28314e..1d8e5ea4f 100644 --- a/pumpkin/src/net/bedrock/play.rs +++ b/pumpkin/src/net/bedrock/play.rs @@ -19,8 +19,8 @@ use pumpkin_protocol::{ text::SText, }, }, - codec::{var_int::VarInt, var_long::VarLong}, - java::client::play::{Animation, CSystemChatMessage}, + codec::{var_int::VarInt, var_long::VarLong, var_ulong::VarULong}, + java::client::play::{Animation, CEntityAnimation, CSystemChatMessage}, }; use pumpkin_util::{GameMode, math::position::BlockPos, text::TextComponent}; @@ -95,6 +95,7 @@ impl BedrockClient { player.set_client_loaded(true); } + #[expect(clippy::too_many_lines)] pub async fn handle_player_auth_input( &self, player: &Arc, @@ -104,16 +105,135 @@ impl BedrockClient { if !player.has_client_loaded() { return; } - let new_pos = packet.position.to_f64(); + let entity = player.get_entity(); + + let new_pos = packet + .position + .add_raw(0.0, -entity.entity_type.eye_height, 0.0) + .to_f64(); let old_pos = player.position(); - if new_pos != old_pos { - player.living_entity.entity.set_pos(new_pos); - chunker::update_position(player).await; + let new_pitch = packet.pitch; + let new_yaw = packet.yaw; + + let old_pitch = entity.pitch.load(); + let old_yaw = entity.yaw.load(); + + let pos_changed = new_pos != old_pos; + let rot_changed = new_pitch != old_pitch || new_yaw != old_yaw; + + if pos_changed || rot_changed { + let world = player.world(); + let on_ground = entity.on_ground.load(std::sync::atomic::Ordering::Relaxed); + + if pos_changed { + player.living_entity.entity.set_pos(new_pos); + } + if rot_changed { + entity.pitch.store(new_pitch); + entity.yaw.store(new_yaw); + } + + let je_yaw = (new_yaw * 256.0 / 360.0).rem_euclid(256.0); + let je_pitch = (new_pitch * 256.0 / 360.0).rem_euclid(256.0); + + let delta = pumpkin_util::math::vector3::Vector3::new( + new_pos.x - old_pos.x, + new_pos.y - old_pos.y, + new_pos.z - old_pos.z, + ); + + let bedrock_move_packet = pumpkin_protocol::bedrock::client::CMovePlayer::new( + pumpkin_protocol::codec::var_ulong::VarULong(player.entity_id() as u64), + pumpkin_util::math::vector3::Vector3::new( + new_pos.x as f32, + new_pos.y as f32 + entity.entity_type.eye_height, + new_pos.z as f32, + ), + new_pitch, + new_yaw, + new_yaw, // Head yaw + pumpkin_protocol::bedrock::client::CMovePlayer::MODE_NORMAL, + on_ground, + pumpkin_protocol::codec::var_ulong::VarULong(0), + 0, + 0, + pumpkin_protocol::codec::var_ulong::VarULong(0), + ); + + if pos_changed && delta.length_squared() >= 64.0 { + world.broadcast_packet_except( + &[player.gameprofile.id], + &pumpkin_protocol::java::client::play::CEntityPositionSync::new( + player.entity_id().into(), + new_pos, + pumpkin_util::math::vector3::Vector3::new(0.0, 0.0, 0.0), + je_yaw, + je_pitch, + on_ground, + ), + ); + } else if pos_changed && rot_changed { + world.broadcast_packet_except_editioned_sync( + &[player.gameprofile.id], + &pumpkin_protocol::java::client::play::CUpdateEntityPosRot::new( + player.entity_id().into(), + pumpkin_util::math::vector3::Vector3::new( + new_pos.x.mul_add(4096.0, -(old_pos.x * 4096.0)) as i16, + new_pos.y.mul_add(4096.0, -(old_pos.y * 4096.0)) as i16, + new_pos.z.mul_add(4096.0, -(old_pos.z * 4096.0)) as i16, + ), + je_yaw as u8, // Use converted Java byte + je_pitch as u8, // Use converted Java byte + on_ground, + ), + &bedrock_move_packet, + ); + } else if pos_changed { + world.broadcast_packet_except_editioned_sync( + &[player.gameprofile.id], + &pumpkin_protocol::java::client::play::CUpdateEntityPos::new( + player.entity_id().into(), + pumpkin_util::math::vector3::Vector3::new( + new_pos.x.mul_add(4096.0, -(old_pos.x * 4096.0)) as i16, + new_pos.y.mul_add(4096.0, -(old_pos.y * 4096.0)) as i16, + new_pos.z.mul_add(4096.0, -(old_pos.z * 4096.0)) as i16, + ), + on_ground, + ), + &bedrock_move_packet, + ); + } else if rot_changed { + world.broadcast_packet_except_editioned_sync( + &[player.gameprofile.id], + &pumpkin_protocol::java::client::play::CUpdateEntityRot::new( + player.entity_id().into(), + je_yaw as u8, // Use converted Java byte + je_pitch as u8, // Use converted Java byte + on_ground, + ), + &bedrock_move_packet, + ); + } + + if rot_changed { + world.broadcast_packet_except( + &[player.gameprofile.id], + // Adjust to `CHeadRot` if that is what your crate currently calls it + &pumpkin_protocol::java::client::play::CHeadRot::new( + player.entity_id().into(), + je_yaw as u8, + ), + ); + } + + if pos_changed { + chunker::update_position(player).await; + player.progress_motion(delta).await; + } } let input_data = packet.input_data; - let entity = player.get_entity(); if input_data.get(InputData::StartSprinting as usize) { entity.set_sprinting(true).await; @@ -189,43 +309,53 @@ impl BedrockClient { .await; } - pub fn handle_animate(&self, player: &Arc, _server: &Server, packet: &SAnimate) { + pub async fn handle_animate(&self, player: &Arc, _server: &Server, packet: &SAnimate) { if !player.has_client_loaded() { return; } let entity = &player.living_entity.entity; - let _world = entity.world.load(); + let world = entity.world.load(); - // Broadcast the animation to other players - let _java_animation = match packet.action { + let java_animation = match packet.action { AnimateAction::SwingArm => Some(Animation::SwingMainArm), AnimateAction::WakeUp => Some(Animation::LeaveBed), AnimateAction::CriticalHit => Some(Animation::CriticalEffect), AnimateAction::MagicCriticalHit => Some(Animation::MagicCriticaleffect), - _ => None, + AnimateAction::StopSleep => None, // TODO }; - // if let Some(animation) = java_animation { - // let je_packet = CEntityAnimation::new(VarInt(entity.entity_id), animation); - // let be_packet = SAnimate { - // action: packet.action, - // runtime_entity_id: VarULong(entity.entity_id as u64), - // boat_rowing_time: packet.boat_rowing_time, - // }; - // world.broadcast_editioned(&je_packet, &be_packet).await; - // } + if let Some(animation) = java_animation { + let je_packet = CEntityAnimation::new(VarInt(entity.entity_id), animation); + let be_packet = SAnimate { + action: packet.action, + runtime_entity_id: VarULong(entity.entity_id as u64), + data: 0.0, + swing_source: None, + }; + world.broadcast_editioned(&je_packet, &be_packet).await; + } } - pub async fn handle_interaction(&self, _player: &Arc, packet: SInteraction) { - if matches!(packet.action, Action::OpenInventory) { - self.send_game_packet(&CContainerOpen { - container_id: 0, - container_type: 0xff, - position: BlockPos::ZERO, - target_entity_id: VarLong(-1), - }) - .await; + pub async fn handle_interaction(&self, player: &Arc, packet: SInteraction) { + match packet.action { + Action::OpenInventory => { + self.send_game_packet(&CContainerOpen { + container_id: 0, + container_type: 0xff, + position: BlockPos::ZERO, + target_entity_id: VarLong(-1), + }) + .await; + } + Action::Attack => { + let target_runtime_id = packet.target_runtime_id.0 as i32; + let world = player.world(); + if let Some(target) = world.get_entity_by_id(target_runtime_id) { + player.attack(target).await; + } + } + _ => {} } } diff --git a/pumpkin/src/net/java/mod.rs b/pumpkin/src/net/java/mod.rs index aa80408c4..fcc9de3fb 100644 --- a/pumpkin/src/net/java/mod.rs +++ b/pumpkin/src/net/java/mod.rs @@ -67,7 +67,7 @@ pub mod recipe_helper; pub mod status; use crate::entity::player::Player; -use crate::net::{GameProfile, PlayerConfig}; +use crate::net::{GameProfile, PacketHandlerResult, PlayerConfig}; use crate::plugin::player::player_custom_payload::PlayerCustomPayloadEvent; use crate::{error::PumpkinError, net::EncryptionError, server::Server}; @@ -112,10 +112,9 @@ pub struct JavaClient { pub last_keep_alive_time: AtomicCell, } -pub enum PacketHandlerResult { - Stop, - // Signal to spawn the player - ReadyToPlay(GameProfile, PlayerConfig), +pub enum OutgoingPacketType { + Normal, + HighPriority, } struct OutgoingPacket { diff --git a/pumpkin/src/net/java/play.rs b/pumpkin/src/net/java/play.rs index 5b27a1d78..b25724660 100644 --- a/pumpkin/src/net/java/play.rs +++ b/pumpkin/src/net/java/play.rs @@ -54,7 +54,9 @@ use pumpkin_inventory::merchant::merchant_screen_handler::MerchantScreenHandler; use pumpkin_inventory::player::player_inventory::PlayerInventory; use pumpkin_inventory::screen_handler::{InventoryPlayer, ScreenHandler}; use pumpkin_macros::send_cancellable; +use pumpkin_protocol::bedrock::client::CMovePlayer; use pumpkin_protocol::codec::var_int::VarInt; +use pumpkin_protocol::codec::var_ulong::VarULong; use pumpkin_protocol::java::client::play::{ CBlockUpdate, CCommandSuggestions, CEntityPositionSync, CHeadRot, COpenSignEditor, CPingResponse, CPlayerInfoUpdate, CPlayerPosition, CSetSelectedSlot, CSystemChatMessage, @@ -309,6 +311,7 @@ impl JavaClient { true } + #[expect(clippy::too_many_lines)] pub async fn handle_position( &self, player: &Arc, @@ -369,20 +372,31 @@ impl JavaClient { // TODO: Warn when player moves to quickly if !Self::sync_position(player, world, pos, last_pos, entity.yaw.load(), entity.pitch.load(), packet.collision & FLAG_ON_GROUND != 0) { // Send the new position to all other players. - world - .broadcast_packet_except( - &[player.gameprofile.id], - &CUpdateEntityPos::new( - player.entity_id().into(), - Vector3::new( - pos.x.mul_add(4096.0, -(last_pos.x * 4096.0)) as i16, - pos.y.mul_add(4096.0, -(last_pos.y * 4096.0)) as i16, - pos.z.mul_add(4096.0, -(last_pos.z * 4096.0)) as i16, - ), - packet.collision & FLAG_ON_GROUND != 0, + world.broadcast_packet_except_editioned_sync( + &[player.gameprofile.id], + &CUpdateEntityPos::new( + player.entity_id().into(), + Vector3::new( + pos.x.mul_add(4096.0, -(last_pos.x * 4096.0)) as i16, + pos.y.mul_add(4096.0, -(last_pos.y * 4096.0)) as i16, + pos.z.mul_add(4096.0, -(last_pos.z * 4096.0)) as i16, ), - ) - ; + packet.collision & FLAG_ON_GROUND != 0, + ), + &CMovePlayer::new( + VarULong(player.entity_id() as u64), + Vector3::new(pos.x as f32, pos.y as f32 + player.get_entity().entity_type.eye_height, pos.z as f32), + entity.pitch.load(), + entity.yaw.load(), + entity.yaw.load(), + CMovePlayer::MODE_NORMAL, + (packet.collision & FLAG_ON_GROUND) != 0, + VarULong(0), + 0, + 0, + VarULong(0), + ), + ); } // Only process fall damage if player is alive @@ -494,22 +508,33 @@ impl JavaClient { sync_position(player, &world, pos, last_pos, yaw, pitch, (packet.collision & FLAG_ON_GROUND) != 0) { // Send the new position to all other players. - world - .broadcast_packet_except( - &[player.gameprofile.id], - &CUpdateEntityPosRot::new( - entity_id.into(), - Vector3::new( - pos.x.mul_add(4096.0, -(last_pos.x * 4096.0)) as i16, - pos.y.mul_add(4096.0, -(last_pos.y * 4096.0)) as i16, - pos.z.mul_add(4096.0, -(last_pos.z * 4096.0)) as i16, - ), - yaw as u8, - pitch as u8, - (packet.collision & FLAG_ON_GROUND) != 0, + world.broadcast_packet_except_editioned_sync( + &[player.gameprofile.id], + &CUpdateEntityPosRot::new( + entity_id.into(), + Vector3::new( + pos.x.mul_add(4096.0, -(last_pos.x * 4096.0)) as i16, + pos.y.mul_add(4096.0, -(last_pos.y * 4096.0)) as i16, + pos.z.mul_add(4096.0, -(last_pos.z * 4096.0)) as i16, ), - ) - ; + yaw as u8, + pitch as u8, + (packet.collision & FLAG_ON_GROUND) != 0, + ), + &CMovePlayer::new( + VarULong(entity_id as u64), + Vector3::new(pos.x as f32, pos.y as f32 + player.get_entity().entity_type.eye_height, pos.z as f32), + entity.pitch.load(), + entity.yaw.load(), + entity.yaw.load(), + CMovePlayer::MODE_NORMAL, + (packet.collision & FLAG_ON_GROUND) != 0, + VarULong(0), + 0, + 0, + VarULong(0), + ), + ); } world @@ -591,11 +616,37 @@ impl JavaClient { // let head_yaw = modulus(entity.head_yaw * 256.0 / 360.0, 256.0); let world = entity.world.load_full(); - let packet = + let je_packet = CUpdateEntityRot::new(entity_id.into(), yaw as u8, pitch as u8, rotation.ground); - world.broadcast_packet_except(&[player.gameprofile.id], &packet); - let packet = CHeadRot::new(entity_id.into(), yaw as u8); - world.broadcast_packet_except(&[player.gameprofile.id], &packet); + + let pos = entity.pos.load(); + + let be_packet = CMovePlayer::new( + VarULong(entity_id as u64), + Vector3::new( + pos.x as f32, + pos.y as f32 + player.get_entity().entity_type.eye_height, + pos.z as f32, + ), + entity.pitch.load(), + entity.yaw.load(), + entity.yaw.load(), + CMovePlayer::MODE_ROTATION, + rotation.ground, + VarULong(0), + 0, + 0, + VarULong(0), + ); + + world.broadcast_packet_except_editioned_sync( + &[player.gameprofile.id], + &je_packet, + &be_packet, + ); + + let je_packet = CHeadRot::new(entity_id.into(), yaw as u8); + world.broadcast_packet_except(&[player.gameprofile.id], &je_packet); } pub async fn handle_chat_command( @@ -1176,7 +1227,7 @@ impl JavaClient { server; event; 'after: { - player.swing_hand(hand, false); + player.swing_hand(hand, false).await; } }} } @@ -2028,7 +2079,7 @@ impl JavaClient { // TODO: Trigger ANY_BLOCK_USE Criteria if matches!(result, BlockActionResult::SuccessServer) { - player.swing_hand(hand, true); + player.swing_hand(hand, true).await; } return Ok(()); } diff --git a/pumpkin/src/net/mod.rs b/pumpkin/src/net/mod.rs index f698e4696..f6eabdfb2 100644 --- a/pumpkin/src/net/mod.rs +++ b/pumpkin/src/net/mod.rs @@ -12,7 +12,7 @@ use std::{ }; use pumpkin_data::translation; -use pumpkin_protocol::{ClientPacket, Property}; +use pumpkin_protocol::{BClientPacket, ClientPacket, Property}; use pumpkin_util::{ Hand, ProfileAction, text::TextComponent, @@ -108,6 +108,11 @@ impl Default for PlayerConfig { } } +pub enum PacketHandlerResult { + Stop, + ReadyToPlay(GameProfile, PlayerConfig), +} + /// This is just a Wrapper for both Java & Bedrock connections #[expect(clippy::large_enum_variant)] pub enum ClientPlatform { @@ -190,24 +195,61 @@ impl ClientPlatform { } } - pub async fn enqueue_packet(&self, packet: &P) { + pub async fn enqueue_packet_editioned( + &self, + je_packet: &J, + be_packet: &B, + ) { match self { - Self::Java(java) => java.enqueue_packet(packet).await, - Self::Bedrock(_) => (), + Self::Java(java) => java.enqueue_packet(je_packet).await, + Self::Bedrock(bedrock) => bedrock.enqueue_packet(be_packet).await, + } + } + + pub fn try_enqueue_packet_editioned( + &self, + je_packet: &J, + be_packet: &B, + ) { + match self { + Self::Java(java) => java.try_enqueue_packet(je_packet), + Self::Bedrock(bedrock) => bedrock.try_enqueue_packet(be_packet), + } + } + + pub async fn enqueue_packet(&self, packet: &P) { + if let Self::Java(java) = self { + java.enqueue_packet(packet).await; + } + } + + pub async fn enqueue_be_packet(&self, packet: &P) { + if let Self::Bedrock(bedrock) = self { + bedrock.enqueue_packet(packet).await; } } pub fn try_enqueue_packet(&self, packet: &P) { - match self { - Self::Java(java) => java.try_enqueue_packet(packet), - Self::Bedrock(_) => (), + if let Self::Java(java) = self { + java.try_enqueue_packet(packet); + } + } + + pub fn try_enqueue_be_packet(&self, packet: &P) { + if let Self::Bedrock(bedrock) = self { + bedrock.try_enqueue_packet(packet); } } pub async fn send_packet_now(&self, packet: &P) { - match self { - Self::Java(java) => java.send_packet_now(packet).await, - Self::Bedrock(_) => (), + if let Self::Java(java) = self { + java.send_packet_now(packet).await; + } + } + + pub async fn send_be_packet_now(&self, packet: &P) { + if let Self::Bedrock(bedrock) = self { + bedrock.send_game_packet(packet).await; } } diff --git a/pumpkin/src/world/mod.rs b/pumpkin/src/world/mod.rs index 959eeb19e..6ec7748a5 100644 --- a/pumpkin/src/world/mod.rs +++ b/pumpkin/src/world/mod.rs @@ -2,7 +2,9 @@ use crate::block::entities::BlockEntity; use dashmap::DashMap; use pumpkin_data::attributes::Attributes; use pumpkin_data::chunk::Biome; +use pumpkin_protocol::bedrock::client::EntityProperties; use pumpkin_protocol::bedrock::client::level_event::{CLevelEvent, LevelEvent}; +use pumpkin_protocol::bedrock::network_item::{ItemInstanceUserData, NetworkItemDescriptor}; use pumpkin_protocol::codec::data_component::data_to_proto_sound; use pumpkin_world::generation::proto_chunk::GenerationCache; use std::sync::atomic::Ordering::Relaxed; @@ -73,6 +75,10 @@ use pumpkin_protocol::bedrock::client::set_actor_data::{ }; 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, +}; use pumpkin_protocol::java::client::play::{ CPlayerSpawnPosition, CRecipeBookAdd, CRecipeBookSettings, CSystemChatMessage, }; @@ -81,15 +87,17 @@ use pumpkin_protocol::{ BClientPacket, ClientPacket, IdOr, SoundEvent, bedrock::{ client::{ + add_player::CAddPlayer, creative_content::{CreativeContent, Group}, gamerules_changed::GameRules, + player_list::{CPlayerList, PlayerListEntry, Skin}, + remove_actor::CRemoveActor, start_game::{Experiments, GamePublishSetting, LevelSettings}, update_attributes::{Attribute, CUpdateAttributes}, }, - network_item::NetworkItemDescriptor, server::text::SText, }, - codec::{var_long::VarLong, var_uint::VarUInt, var_ulong::VarULong}, + codec::{var_int::VarInt, var_long::VarLong, var_uint::VarUInt, var_ulong::VarULong}, java::{ self, client::play::{ @@ -105,13 +113,7 @@ use pumpkin_protocol::{ codec::item_stack_seralizer::ItemStackSerializer, java::client::play::{CBlockEvent, CRemoveMobEffect, CSetEquipment, CUpdateMobEffect}, }; -use pumpkin_protocol::{ - codec::var_int::VarInt, - java::client::play::{ - CBlockUpdate, CChunkBatchEnd, CChunkBatchStart, CChunkData, CDisguisedChatMessage, - CExplosion, CRespawn, CSetBlockDestroyStage, CWorldEvent, - }, -}; +use pumpkin_util::GameMode; use pumpkin_util::resource_location::ResourceLocation; use pumpkin_util::text::{TextComponent, color::NamedColor}; use pumpkin_util::version::JavaMinecraftVersion; @@ -618,6 +620,59 @@ impl World { sender.chat_session.lock().await.messages_sent += 1; } + pub fn broadcast_packet_except_editioned_sync( + &self, + except: &[uuid::Uuid], + je_packet: &J, + be_packet: &B, + ) { + let players = self.players.load(); + let mut java_recipients = Vec::new(); + + for p in players.iter() { + if except.contains(&p.gameprofile.id) { + continue; + } + match &p.client { + ClientPlatform::Java(_) => java_recipients.push(p), + ClientPlatform::Bedrock(be_client) => be_client.try_enqueue_packet(be_packet), + } + } + + let recipients_by_version = + Self::collect_java_recipients_by_version(java_recipients.into_iter()); + Self::broadcast_java_grouped(je_packet, recipients_by_version); + } + + pub async fn broadcast_packet_except_editioned( + &self, + except: &[uuid::Uuid], + je_packet: &J, + be_packet: &B, + ) { + let players = self.players.load(); + let mut java_recipients = Vec::new(); + let mut bedrock_recipients = Vec::new(); + + for p in players.iter() { + if except.contains(&p.gameprofile.id) { + continue; + } + match &p.client { + ClientPlatform::Java(_) => java_recipients.push(p), + ClientPlatform::Bedrock(be_client) => bedrock_recipients.push(be_client.clone()), + } + } + + let recipients_by_version = + Self::collect_java_recipients_by_version(java_recipients.into_iter()); + Self::broadcast_java_grouped(je_packet, recipients_by_version); + + for be_client in bedrock_recipients { + be_client.enqueue_packet(be_packet).await; + } + } + /// Broadcasts a packet to all connected players within the world, excluding the specified players. /// /// Sends the specified packet to every player currently logged in to the world, excluding the players listed in the `except` parameter. @@ -1597,6 +1652,7 @@ impl World { let level_info = server.level_info.load(); let weather = self.weather.lock().await; let runtime_id = player.entity_id() as u64; + let (position, yaw, pitch) = if player.has_played_before.load(Ordering::Relaxed) { let position = player.position(); let yaw = player.living_entity.entity.yaw.load(); //info.spawn_angle; @@ -1616,6 +1672,7 @@ impl World { ); (position, level_info.spawn_yaw, level_info.spawn_pitch) }; + // Todo make the data less spread let level_settings = LevelSettings { seed: self.level.seed.0, @@ -1708,17 +1765,12 @@ impl World { // TODO Make this unique multiplayer_correlation_id: Uuid::default().to_string(), enable_itemstack_net_manager: true, - // TODO Make this description better! - // This gets send from the client to mojang for telemetry server_version: "Pumpkin Rust Server".to_string(), - compound_id: 10, compound_len: VarUInt(0), compound_end: 0, - block_registry_checksum: 0, world_template_id: Uuid::nil(), - // TODO The client needs extra biome data for this enable_clientside_generation: false, blocknetwork_ids_are_hashed: false, server_auth_sounds: false, @@ -1731,7 +1783,9 @@ impl World { }, }) .await; + chunker::update_position(&player).await; + client .send_game_packet(&CreativeContent { groups: &[Group { @@ -1747,8 +1801,8 @@ impl World { let mut abilities = player.abilities.lock().await; abilities.set_for_gamemode(player.gamemode.load()); }; - let mut metadata = EntityMetadata::new(); + let mut metadata = EntityMetadata::new(); metadata.set(entity_data_key::WIDTH, MetadataValue::Float(0.6)); metadata.set(entity_data_key::HEIGHT, MetadataValue::Float(1.8)); @@ -1781,18 +1835,15 @@ impl World { tick: VarULong(0), }; client.send_game_packet(&actor_data).await; - player.send_abilities_update().await; + { let command_dispatcher = server.command_dispatcher.read().await; - client_suggestions::send_bedrock_commands_packet(&player, server, &command_dispatcher) .await; }; let mut frame_set = FrameSet::default(); - - // https://github.com/pmmp/PocketMine-MP/blob/0b6d8f8cb2aaa05ffad0b6386bd88d73ef54b395/src/entity/AttributeFactory.php#L34 client .write_game_packet_to_set( &CUpdateAttributes { @@ -1865,6 +1916,188 @@ impl World { ) .await; client.send_frame_set(frame_set, 0x84).await; + + // --- MULTIPLAYER BROADCASTING --- + + let gameprofile = &player.gameprofile; + let velocity = player.living_entity.entity.velocity.load(); + + // 1. Broadcast the new Bedrock player to everyone else (Java + Bedrock) + let bedrock_player_list = CPlayerList { + action: CPlayerList::ACTION_ADD, + entries: vec![PlayerListEntry { + uuid: gameprofile.id, + entity_unique_id: VarLong(runtime_id as i64), + username: gameprofile.name.clone(), + xuid: String::new(), + platform_chat_id: String::new(), + build_platform: 0, + skin: (**player.bedrock_skin.load()).clone(), + is_teacher: false, + is_host: false, + is_sub_client: false, + player_color: [0, 0, 0, 0], + }], + }; + + self.broadcast_packet_except_editioned_sync( + &[gameprofile.id], + &CPlayerInfoUpdate::new( + PlayerInfoFlags::ADD_PLAYER.bits(), + &[pumpkin_protocol::java::client::play::Player { + uuid: gameprofile.id, + actions: &[PlayerAction::AddPlayer { + name: &gameprofile.name, + properties: &gameprofile.properties.load(), + }], + }], + ), + &bedrock_player_list, + ); + + let bedrock_add_player = CAddPlayer { + uuid: gameprofile.id, + username: gameprofile.name.clone(), + entity_runtime_id: VarULong(runtime_id), + platform_chat_id: String::new(), + position: Vector3::new(position.x as f32, position.y as f32, position.z as f32), + velocity: Vector3::new(velocity.x as f32, velocity.y as f32, velocity.z as f32), + pitch, + yaw, + head_yaw: yaw, + held_item: NetworkItemDescriptor::default(), + game_mode: VarInt(match player.gamemode.load() { + GameMode::Survival => 0, + GameMode::Creative => 1, + GameMode::Adventure => 2, + GameMode::Spectator => 6, + }), + metadata: EntityMetadata::default(), + properties: EntityProperties::default(), + ability_data: pumpkin_protocol::bedrock::client::add_player::AbilityData { + entity_unique_id: runtime_id as i64, + player_permissions: 0, + command_permissions: 0, + layers: vec![pumpkin_protocol::bedrock::client::AbilityLayer { + serialized_layer: 0, + abilities_set: 0, + ability_value: 0, + fly_speed: 0.05, + vertical_fly_speed: 0.05, + walk_speed: 0.1, + }], + }, + links: Vec::new(), + device_id: String::new(), + build_platform: 0, + }; + + self.broadcast_packet_except_editioned_sync( + &[gameprofile.id], + &CSpawnEntity::new( + (runtime_id as i32).into(), + gameprofile.id, + i32::from(EntityType::PLAYER.id).into(), + position, + pitch, + yaw, + yaw, + 0.into(), + velocity, + ), + &bedrock_add_player, + ); + + // 2. Spawn existing players for our new Bedrock client + let mut existing_entries = Vec::new(); + let players = self.players.load(); + + for existing_player in players + .iter() + .filter(|p| p.gameprofile.id != gameprofile.id) + { + let ex_profile = &existing_player.gameprofile; + let ex_entity = &existing_player.living_entity.entity; + let ex_pos = ex_entity.pos.load(); + let ex_vel = ex_entity.velocity.load(); + + existing_entries.push(PlayerListEntry { + uuid: ex_profile.id, + entity_unique_id: VarLong(existing_player.entity_id() as i64), + username: ex_profile.name.clone(), + xuid: String::new(), + platform_chat_id: String::new(), + build_platform: 0, + skin: (**existing_player.bedrock_skin.load()).clone(), + is_teacher: false, + is_host: false, + is_sub_client: false, + player_color: [0, 0, 0, 0], + }); + + let ex_add_player = CAddPlayer { + uuid: ex_profile.id, + username: ex_profile.name.clone(), + entity_runtime_id: VarULong(existing_player.entity_id() as u64), + platform_chat_id: String::new(), + position: Vector3::new(ex_pos.x as f32, ex_pos.y as f32, ex_pos.z as f32), + velocity: Vector3::new(ex_vel.x as f32, ex_vel.y as f32, ex_vel.z as f32), + pitch: ex_entity.pitch.load(), + yaw: ex_entity.yaw.load(), + head_yaw: ex_entity.head_yaw.load(), + held_item: NetworkItemDescriptor::default(), + game_mode: VarInt(match existing_player.gamemode.load() { + GameMode::Survival => 0, + GameMode::Creative => 1, + GameMode::Adventure => 2, + GameMode::Spectator => 6, + }), + metadata: EntityMetadata::default(), + properties: EntityProperties::default(), + ability_data: pumpkin_protocol::bedrock::client::add_player::AbilityData { + entity_unique_id: existing_player.entity_id() as i64, + player_permissions: 0, + command_permissions: 0, + layers: vec![pumpkin_protocol::bedrock::client::AbilityLayer { + serialized_layer: 0, + abilities_set: 0, + ability_value: 0, + fly_speed: 0.05, + vertical_fly_speed: 0.05, + walk_speed: 0.1, + }], + }, + links: Vec::new(), + device_id: String::new(), + build_platform: 0, + }; + + client.send_game_packet(&ex_add_player).await; + } + + if !existing_entries.is_empty() { + let ex_player_list = CPlayerList { + action: CPlayerList::ACTION_ADD, + entries: existing_entries, + }; + client.send_game_packet(&ex_player_list).await; + } + + // 3. Trigger Join Event and Broadcast Join Message + let msg_comp = TextComponent::translate_cross( + translation::java::MULTIPLAYER_PLAYER_JOINED, + translation::bedrock::MULTIPLAYER_PLAYER_JOINED, + [TextComponent::text(player.gameprofile.name.clone())], + ) + .color_named(NamedColor::Yellow); + + let event = PlayerJoinEvent::new(player.clone(), msg_comp); + let event = server.plugin_manager.fire(event).await; + + if !event.cancelled { + self.broadcast_system_message(&event.join_message, false); + info!("{}", event.join_message.to_pretty_console()); + } } #[expect(clippy::too_many_lines)] @@ -2093,9 +2326,84 @@ impl World { let gameprofile = &player.gameprofile; - debug!("Broadcasting player spawn for {}", player.gameprofile.name); + let bedrock_add_player = CAddPlayer { + uuid: gameprofile.id, + username: gameprofile.name.clone(), + entity_runtime_id: VarULong(entity_id as u64), + platform_chat_id: String::new(), + position: Vector3::new(position.x as f32, position.y as f32, position.z as f32), + velocity: Vector3::new(velocity.x as f32, velocity.y as f32, velocity.z as f32), + pitch, + yaw, + head_yaw: yaw, + held_item: NetworkItemDescriptor { + id: VarInt(0), + stack_size: 0, + aux_value: VarUInt(0), + block_runtime_id: VarInt(0), + user_data_buffer: ItemInstanceUserData::default(), + }, + game_mode: VarInt(match player.gamemode.load() { + GameMode::Survival => 0, + GameMode::Creative => 1, + GameMode::Adventure => 2, + GameMode::Spectator => 6, + }), + metadata: EntityMetadata::default(), + properties: EntityProperties::default(), + ability_data: pumpkin_protocol::bedrock::client::add_player::AbilityData { + entity_unique_id: entity_id as i64, + player_permissions: 0, + command_permissions: 0, + layers: vec![pumpkin_protocol::bedrock::client::AbilityLayer { + serialized_layer: 0, + abilities_set: 0, + ability_value: 0, + fly_speed: 0.05, + vertical_fly_speed: 0.05, + walk_speed: 0.1, + }], + }, + links: Vec::new(), + device_id: String::new(), + build_platform: 0, + }; + + let bedrock_player_list = CPlayerList { + action: CPlayerList::ACTION_ADD, + entries: vec![PlayerListEntry { + uuid: gameprofile.id, + entity_unique_id: VarLong(entity_id as i64), + username: gameprofile.name.clone(), + xuid: String::new(), + platform_chat_id: String::new(), + build_platform: 0, + skin: (**player.bedrock_skin.load()).clone(), + is_teacher: false, + + is_host: false, + is_sub_client: false, + player_color: [0, 0, 0, 0], + }], + }; + + self.broadcast_packet_except_editioned_sync( + &[player.gameprofile.id], + &CPlayerInfoUpdate::new( + PlayerInfoFlags::ADD_PLAYER.bits(), + &[pumpkin_protocol::java::client::play::Player { + uuid: gameprofile.id, + actions: &[PlayerAction::AddPlayer { + name: &gameprofile.name, + properties: &gameprofile.properties.load(), + }], + }], + ), + &bedrock_player_list, + ); + // Spawn the player for every client. - self.broadcast_packet_except( + self.broadcast_packet_except_editioned_sync( &[player.gameprofile.id], &CSpawnEntity::new( entity_id.into(), @@ -2108,6 +2416,7 @@ impl World { 0.into(), velocity, ), + &bedrock_add_player, ); // Spawn players for our client. @@ -2121,20 +2430,103 @@ impl World { let entity = &existing_player.living_entity.entity; let pos = entity.pos.load(); let gameprofile = &existing_player.gameprofile; - debug!("Sending player entities to {}", player.gameprofile.name); + let bedrock_add_player = CAddPlayer { + uuid: gameprofile.id, + username: gameprofile.name.clone(), + entity_runtime_id: VarULong(existing_player.entity_id() as u64), + platform_chat_id: String::new(), + position: Vector3::new(pos.x as f32, pos.y as f32, pos.z as f32), + velocity: Vector3::new( + entity.velocity.load().x as f32, + entity.velocity.load().y as f32, + entity.velocity.load().z as f32, + ), + pitch: entity.pitch.load(), + yaw: entity.yaw.load(), + head_yaw: entity.head_yaw.load(), + held_item: NetworkItemDescriptor { + id: VarInt(0), + stack_size: 0, + aux_value: VarUInt(0), + block_runtime_id: VarInt(0), + user_data_buffer: ItemInstanceUserData::default(), + }, + game_mode: VarInt(match existing_player.gamemode.load() { + GameMode::Survival => 0, + GameMode::Creative => 1, + GameMode::Adventure => 2, + GameMode::Spectator => 6, + }), + metadata: EntityMetadata::default(), + properties: EntityProperties::default(), + ability_data: pumpkin_protocol::bedrock::client::add_player::AbilityData { + entity_unique_id: existing_player.entity_id() as i64, + player_permissions: 0, + command_permissions: 0, + layers: vec![pumpkin_protocol::bedrock::client::AbilityLayer { + serialized_layer: 0, + abilities_set: 0, + ability_value: 0, + fly_speed: 0.05, + vertical_fly_speed: 0.05, + walk_speed: 0.1, + }], + }, + links: Vec::new(), + device_id: String::new(), + build_platform: 0, + }; - client - .enqueue_packet(&CSpawnEntity::new( - existing_player.entity_id().into(), - gameprofile.id, - i32::from(EntityType::PLAYER.id).into(), - pos, - entity.pitch.load(), - entity.yaw.load(), - entity.head_yaw.load(), - 0.into(), - entity.velocity.load(), - )) + let bedrock_player_list = CPlayerList { + action: CPlayerList::ACTION_ADD, + entries: vec![PlayerListEntry { + uuid: gameprofile.id, + entity_unique_id: VarLong(existing_player.entity_id() as i64), + username: gameprofile.name.clone(), + xuid: String::new(), + platform_chat_id: String::new(), + build_platform: 0, + skin: (**existing_player.bedrock_skin.load()).clone(), + is_teacher: false, + is_host: false, + is_sub_client: false, + player_color: [0, 0, 0, 0], + }], + }; + + player + .client + .enqueue_packet_editioned( + &CPlayerInfoUpdate::new( + PlayerInfoFlags::ADD_PLAYER.bits(), + &[pumpkin_protocol::java::client::play::Player { + uuid: gameprofile.id, + actions: &[PlayerAction::AddPlayer { + name: &gameprofile.name, + properties: &gameprofile.properties.load(), + }], + }], + ), + &bedrock_player_list, + ) + .await; + + player + .client + .enqueue_packet_editioned( + &CSpawnEntity::new( + existing_player.entity_id().into(), + gameprofile.id, + i32::from(EntityType::PLAYER.id).into(), + pos, + entity.pitch.load(), + entity.yaw.load(), + entity.head_yaw.load(), + 0.into(), + entity.velocity.load(), + ), + &bedrock_add_player, + ) .await; { let config = existing_player.config.load(); @@ -3145,8 +3537,33 @@ impl World { }); if let Some(ref player) = removed_player { let uuid = player.gameprofile.id; - self.broadcast_packet_all(&CRemovePlayerInfo::new(&[uuid])); - self.broadcast_packet_all(&CRemoveEntities::new(&[player.entity_id().into()])); + let entity_id = player.entity_id(); + + let bedrock_remove_player = CPlayerList { + action: CPlayerList::ACTION_REMOVE, + entries: vec![PlayerListEntry { + uuid, + entity_unique_id: VarLong(entity_id as i64), + username: player.gameprofile.name.clone(), + xuid: String::new(), + platform_chat_id: String::new(), + build_platform: 0, + skin: Skin::steve(), + is_teacher: false, + is_host: false, + is_sub_client: false, + player_color: [0, 0, 0, 0], + }], + }; + + self.broadcast_editioned(&CRemovePlayerInfo::new(&[uuid]), &bedrock_remove_player) + .await; + + self.broadcast_editioned( + &CRemoveEntities::new(&[entity_id.into()]), + &CRemoveActor::new(VarLong(entity_id as i64)), + ) + .await; if fire_event { let msg_comp = TextComponent::translate_cross( @@ -4273,6 +4690,33 @@ impl World { Self::broadcast_java_grouped(packet, recipients_by_version); } + pub fn broadcast_to_chunk_editioned_sync( + &self, + chunk_pos: Vector2, + je_packet: &J, + be_packet: &B, + ) { + let players = self.players.load(); + let mut java_recipients = Vec::new(); + + let recipients = players.iter().filter(|p| { + let center = p.living_entity.entity.chunk_pos.load(); + let view_distance = get_view_distance(p).get() as i32; + is_within_view_distance(chunk_pos, center, view_distance) + }); + + for p in recipients { + match &p.client { + ClientPlatform::Java(_) => java_recipients.push(p), + ClientPlatform::Bedrock(be_client) => be_client.try_enqueue_packet(be_packet), + } + } + + let recipients_by_version = + Self::collect_java_recipients_by_version(java_recipients.into_iter()); + Self::broadcast_java_grouped(je_packet, recipients_by_version); + } + /// Broadcasts a packet to chunk watchers, excluding specific players. pub fn broadcast_to_chunk_except( &self,