feat: bedrock & java cross play

This commit is contained in:
Alexander Medvedev 2026-05-25 11:53:37 +02:00
parent 100b8791cf
commit e1967bed2b
30 changed files with 2180 additions and 324 deletions

64
Cargo.lock generated
View File

@ -250,12 +250,24 @@ dependencies = [
"allocator-api2", "allocator-api2",
] ]
[[package]]
name = "bytemuck"
version = "1.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec"
[[package]] [[package]]
name = "byteorder" name = "byteorder"
version = "1.5.0" version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "byteorder-lite"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
[[package]] [[package]]
name = "bytes" name = "bytes"
version = "1.11.1" version = "1.11.1"
@ -1173,6 +1185,15 @@ version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" 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]] [[package]]
name = "ff" name = "ff"
version = "0.13.1" version = "0.13.1"
@ -1775,6 +1796,19 @@ dependencies = [
"icu_properties", "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]] [[package]]
name = "indexmap" name = "indexmap"
version = "2.14.0" version = "2.14.0"
@ -2091,6 +2125,16 @@ dependencies = [
"windows-sys 0.61.2", "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]] [[package]]
name = "nix" name = "nix"
version = "0.31.3" version = "0.31.3"
@ -2472,6 +2516,19 @@ version = "0.3.33"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" 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]] [[package]]
name = "postcard" name = "postcard"
version = "1.1.3" version = "1.1.3"
@ -2619,6 +2676,7 @@ dependencies = [
"futures", "futures",
"hmac 0.13.0", "hmac 0.13.0",
"hyper", "hyper",
"image",
"libloading", "libloading",
"notify", "notify",
"num-bigint", "num-bigint",
@ -2841,6 +2899,12 @@ dependencies = [
"xxhash-rust", "xxhash-rust",
] ]
[[package]]
name = "pxfm"
version = "0.1.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0c5ccf5294c6ccd63a74f1565028353830a9c2f5eb0c682c355c471726a6e3f"
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.45" version = "1.0.45"

View File

@ -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<f32>,
pub velocity: Vector3<f32>,
pub pitch: f32,
pub yaw: f32,
pub head_yaw: f32,
pub body_yaw: f32,
pub attributes: Vec<AttributeValue>,
pub metadata: EntityMetadata,
pub synced_properties: PropertySyncData,
pub links: Vec<EntityLink>,
}
impl PacketWrite for CAddActor {
fn write<W: 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<f32>,
velocity: Vector3<f32>,
pitch: f32,
yaw: f32,
head_yaw: f32,
body_yaw: f32,
attributes: Vec<AttributeValue>,
metadata: EntityMetadata,
synced_properties: PropertySyncData,
links: Vec<EntityLink>,
) -> 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,
}

View File

@ -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<f32>,
pub velocity: Vector3<f32>,
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<EntityLink>,
pub device_id: String,
pub build_platform: i32,
}
impl PacketWrite for CAddPlayer {
fn write<W: 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<f32>,
velocity: Vector3<f32>,
pitch: f32,
yaw: f32,
head_yaw: f32,
held_item: NetworkItemDescriptor,
game_mode: VarInt,
metadata: EntityMetadata,
properties: EntityProperties,
ability_data: AbilityData,
links: Vec<EntityLink>,
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<W: 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<AbilityLayer>,
}
impl PacketWrite for AbilityData {
fn write<W: 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(())
}
}

View File

@ -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,
}

View File

@ -1,6 +1,9 @@
pub mod add_actor;
pub mod add_player;
pub mod available_commands; pub mod available_commands;
pub mod boss_event; pub mod boss_event;
pub mod chunk_radius_update; pub mod chunk_radius_update;
pub mod common;
pub mod container_open; pub mod container_open;
pub mod correct_player_move; pub mod correct_player_move;
pub mod creative_content; pub mod creative_content;
@ -11,12 +14,16 @@ pub mod inventory_content;
pub mod level_chunk; pub mod level_chunk;
pub mod level_event; pub mod level_event;
pub mod modal_form_request; pub mod modal_form_request;
pub mod move_actor_absolute;
pub mod move_actor_delta;
pub mod move_player; pub mod move_player;
pub mod network_chunk_publisher_update; pub mod network_chunk_publisher_update;
pub mod network_settings; pub mod network_settings;
pub mod play_status; pub mod play_status;
pub mod player_hotbar; pub mod player_hotbar;
pub mod player_list;
pub mod raknet; pub mod raknet;
pub mod remove_actor;
pub mod resource_pack_stack; pub mod resource_pack_stack;
pub mod resource_packs_info; pub mod resource_packs_info;
pub mod scoreboard; pub mod scoreboard;
@ -31,9 +38,12 @@ pub mod transfer;
pub mod update_abilities; pub mod update_abilities;
pub mod update_attributes; pub mod update_attributes;
pub use add_actor::*;
pub use add_player::*;
pub use available_commands::*; pub use available_commands::*;
pub use boss_event::*; pub use boss_event::*;
pub use chunk_radius_update::*; pub use chunk_radius_update::*;
pub use common::*;
pub use container_open::*; pub use container_open::*;
pub use correct_player_move::*; pub use correct_player_move::*;
pub use creative_content::*; pub use creative_content::*;
@ -44,12 +54,16 @@ pub use inventory_content::*;
pub use level_chunk::*; pub use level_chunk::*;
pub use level_event::*; pub use level_event::*;
pub use modal_form_request::*; pub use modal_form_request::*;
pub use move_actor_absolute::*;
pub use move_actor_delta::*;
pub use move_player::*; pub use move_player::*;
pub use network_chunk_publisher_update::*; pub use network_chunk_publisher_update::*;
pub use network_settings::*; pub use network_settings::*;
pub use play_status::*; pub use play_status::*;
pub use player_hotbar::*; pub use player_hotbar::*;
pub use player_list::*;
pub use raknet::*; pub use raknet::*;
pub use remove_actor::*;
pub use resource_pack_stack::*; pub use resource_pack_stack::*;
pub use resource_packs_info::*; pub use resource_packs_info::*;
pub use scoreboard::*; pub use scoreboard::*;

View File

@ -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<f32>,
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<f32>,
pitch: u8,
yaw: u8,
head_yaw: u8,
) -> Self {
Self {
entity_runtime_id,
flags,
position,
pitch,
yaw,
head_yaw,
}
}
}

View File

@ -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<W: 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(())
}
}

View File

@ -1,12 +1,11 @@
use crate::{codec::var_ulong::VarULong, serial::PacketWrite};
use pumpkin_macros::packet; use pumpkin_macros::packet;
use pumpkin_util::math::vector3::Vector3; use pumpkin_util::math::vector3::Vector3;
use std::io::{Error, Write};
use crate::{codec::var_ulong::VarULong, serial::PacketWrite}; #[derive(Debug)]
#[derive(Debug, PacketWrite)]
#[packet(19)] #[packet(19)]
pub struct CMovePlayer { pub struct CMovePlayer {
// https://mojang.github.io/bedrock-protocol-docs/html/MovePlayerPacket.html
pub player_runtime_id: VarULong, pub player_runtime_id: VarULong,
pub position: Vector3<f32>, pub position: Vector3<f32>,
pub pitch: f32, pub pitch: f32,
@ -15,6 +14,8 @@ pub struct CMovePlayer {
pub mode: u8, pub mode: u8,
pub on_ground: bool, pub on_ground: bool,
pub riding_runtime_id: VarULong, pub riding_runtime_id: VarULong,
pub teleport_cause: i32,
pub teleport_source_entity_type: i32,
pub tick: VarULong, pub tick: VarULong,
} }
@ -22,5 +23,54 @@ impl CMovePlayer {
pub const MODE_NORMAL: u8 = 0; pub const MODE_NORMAL: u8 = 0;
pub const MODE_RESET: u8 = 1; pub const MODE_RESET: u8 = 1;
pub const MODE_TELEPORT: u8 = 2; 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<f32>,
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<W: 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(())
}
} }

View File

@ -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<PlayerListEntry>,
}
impl PacketWrite for CPlayerList {
fn write<W: 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<W: 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<u8>,
pub image_width: u32,
pub image_height: u32,
pub skin_data: Vec<u8>,
pub animations: Vec<SkinAnimation>,
pub cape_width: u32,
pub cape_height: u32,
pub cape_data: Vec<u8>,
pub geometry_data: Vec<u8>,
pub animation_data: Vec<u8>,
pub geometry_data_engine_version: Vec<u8>,
pub cape_id: String,
pub full_id: String,
pub arm_size: String,
pub skin_color: String,
pub persona_pieces: Vec<PersonaPiece>,
pub piece_tint_colors: Vec<PieceTintColor>,
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<W: 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<u8>,
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<String>,
}

View File

@ -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 }
}
}

View File

@ -10,3 +10,14 @@ pub struct CSetActorMotion {
pub motion: Vector3<f32>, pub motion: Vector3<f32>,
pub tick: VarULong, pub tick: VarULong,
} }
impl CSetActorMotion {
#[must_use]
pub const fn new(target_runtime_id: VarULong, motion: Vector3<f32>, tick: VarULong) -> Self {
Self {
target_runtime_id,
motion,
tick,
}
}
}

View File

@ -2,6 +2,8 @@ use pumpkin_macros::packet;
use crate::serial::PacketWrite; use crate::serial::PacketWrite;
use super::common::AbilityLayer;
#[packet(187)] #[packet(187)]
#[derive(PacketWrite)] #[derive(PacketWrite)]
pub struct CUpdateAbilities { pub struct CUpdateAbilities {
@ -13,17 +15,6 @@ pub struct CUpdateAbilities {
pub layers: Vec<AbilityLayer>, pub layers: Vec<AbilityLayer>,
} }
#[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)] #[repr(u32)]
pub enum Ability { pub enum Ability {
Build = 0, Build = 0,

View File

@ -1,4 +1,7 @@
use std::io::{Error, Read, Write}; use std::{
io::{Error, Read, Write},
str::FromStr,
};
use pumpkin_macros::packet; use pumpkin_macros::packet;
@ -7,36 +10,24 @@ use crate::{
serial::{PacketRead, PacketWrite}, serial::{PacketRead, PacketWrite},
}; };
#[derive(Debug)]
#[packet(44)]
pub struct SAnimate {
pub action: AnimateAction,
pub runtime_entity_id: VarULong,
pub boat_rowing_time: Option<f32>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AnimateAction { pub enum AnimateAction {
NoAction = 0,
SwingArm = 1, SwingArm = 1,
WakeUp = 2, WakeUp = 2,
CriticalHit = 3, StopSleep = 3,
MagicCriticalHit = 4, CriticalHit = 4,
RowRight = 128, MagicCriticalHit = 5,
RowLeft = 129,
} }
impl PacketRead for AnimateAction { impl PacketRead for AnimateAction {
fn read<R: Read>(reader: &mut R) -> Result<Self, Error> { fn read<R: Read>(reader: &mut R) -> Result<Self, Error> {
let action = u8::read(reader)?; let action = u8::read(reader)?;
match action { match action {
0 => Ok(Self::NoAction),
1 => Ok(Self::SwingArm), 1 => Ok(Self::SwingArm),
2 => Ok(Self::WakeUp), 2 => Ok(Self::WakeUp),
3 => Ok(Self::CriticalHit), 3 => Ok(Self::StopSleep),
4 => Ok(Self::MagicCriticalHit), 4 => Ok(Self::CriticalHit),
128 => Ok(Self::RowRight), 5 => Ok(Self::MagicCriticalHit),
129 => Ok(Self::RowLeft),
_ => Err(Error::other(format!("Invalid animate action ID: {action}"))), _ => 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<Self, Error> {
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<AnimateSwingSource>,
}
impl PacketRead for SAnimate { impl PacketRead for SAnimate {
fn read<R: Read>(reader: &mut R) -> Result<Self, Error> { fn read<R: Read>(reader: &mut R) -> Result<Self, Error> {
let action = AnimateAction::read(reader)?; let action = AnimateAction::read(reader)?;
let runtime_entity_id = VarULong::read(reader)?; let runtime_entity_id = VarULong::read(reader)?;
let boat_rowing_time = let data = f32::read(reader)?;
if action == AnimateAction::RowRight || action == AnimateAction::RowLeft {
Some(f32::read(reader)?) let swing_source_str = Option::<String>::read(reader)?;
} else { let swing_source = match swing_source_str {
None Some(s) => Some(AnimateSwingSource::from_str(&s)?),
None => None,
}; };
let _swing_source = bool::read(reader)?;
Ok(Self { Ok(Self {
action, action,
runtime_entity_id, runtime_entity_id,
boat_rowing_time, data,
swing_source,
}) })
} }
} }
@ -71,11 +122,11 @@ impl PacketWrite for SAnimate {
fn write<W: Write>(&self, writer: &mut W) -> Result<(), Error> { fn write<W: Write>(&self, writer: &mut W) -> Result<(), Error> {
self.action.write(writer)?; self.action.write(writer)?;
self.runtime_entity_id.write(writer)?; self.runtime_entity_id.write(writer)?;
if let Some(rowing_time) = self.boat_rowing_time { self.data.write(writer)?;
rowing_time.write(writer)?;
} let swing_source_str: Option<String> = self.swing_source.map(|s| s.as_str().to_string());
// swingSource swing_source_str.write(writer)?;
false.write(writer)?;
Ok(()) Ok(())
} }
} }

View File

@ -18,6 +18,8 @@ pub struct SInteraction {
#[repr(i8)] #[repr(i8)]
pub enum Action { pub enum Action {
Invalid = 0, Invalid = 0,
Interact = 1,
Attack = 2,
StopRiding = 3, StopRiding = 3,
InteractUpdate = 4, InteractUpdate = 4,
NpcOpen = 5, NpcOpen = 5,
@ -31,6 +33,8 @@ impl PacketRead for Action {
let this = match byte[0] { let this = match byte[0] {
0 => Self::Invalid, 0 => Self::Invalid,
1 => Self::Interact,
2 => Self::Attack,
3 => Self::StopRiding, 3 => Self::StopRiding,
4 => Self::InteractUpdate, 4 => Self::InteractUpdate,
5 => Self::NpcOpen, 5 => Self::NpcOpen,

View File

@ -199,7 +199,7 @@ pub enum InputData {
StartFlying = 42, StartFlying = 42,
StopFlying = 43, StopFlying = 43,
ClientAckServerData = 44, ClientAckServerData = 44,
ClientPredictedVehicle = 45, // Renamed to match Go ClientPredictedVehicle = 45,
PaddlingLeft = 46, PaddlingLeft = 46,
PaddlingRight = 47, PaddlingRight = 47,
BlockBreakingDelayEnabled = 48, BlockBreakingDelayEnabled = 48,

View File

@ -20,7 +20,7 @@ pub type VarLongType = i64;
/** /**
* A variable-length long type used by the Minecraft network protocol. * 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); pub struct VarLong(pub VarLongType);
impl VarLong { impl VarLong {

View File

@ -20,7 +20,7 @@ pub type VarULongType = u64;
/** /**
* A variable-length long type used by the Minecraft network protocol. * 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); pub struct VarULong(pub VarULongType);
impl VarULong { impl VarULong {

View File

@ -109,6 +109,7 @@ postcard = { workspace = true }
tracing-serde-structured = { workspace = true } tracing-serde-structured = { workspace = true }
bitflags = { workspace = true } bitflags = { workspace = true }
image = { version = "0.25.10", default-features = false, features = ["png"] }
[dev-dependencies] [dev-dependencies]
tempfile.workspace = true tempfile.workspace = true

View File

@ -197,7 +197,7 @@ impl Goal for MeleeAttackGoal {
.await .await
{ {
self.cooldown = self.get_max_cooldown(); 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; mob.get_mob_entity().try_attack(mob, target.as_ref()).await;
} }
}) })

View File

@ -47,8 +47,7 @@ use pumpkin_nbt::compound::NbtCompound;
use pumpkin_nbt::tag::NbtTag; use pumpkin_nbt::tag::NbtTag;
use pumpkin_protocol::codec::var_int::VarInt; use pumpkin_protocol::codec::var_int::VarInt;
use pumpkin_protocol::java::client::play::{ use pumpkin_protocol::java::client::play::{
Animation, CEntityAnimation, CEntityStatus, CHurtAnimation, CSetPlayerInventory, CEntityStatus, CHurtAnimation, CSetPlayerInventory, CTakeItemEntity, CUpdateMobEffect,
CTakeItemEntity, CUpdateMobEffect,
}; };
use pumpkin_protocol::{ use pumpkin_protocol::{
codec::item_stack_seralizer::ItemStackSerializer, codec::item_stack_seralizer::ItemStackSerializer,
@ -715,15 +714,22 @@ impl LivingEntity {
} }
} }
pub fn swing_hand(&self) { pub async fn swing_hand(&self) {
// TODO: radius let world = self.entity.world.load();
self.entity let entity_id = self.entity_id();
.world
.load() let je_packet = pumpkin_protocol::java::client::play::CEntityAnimation::new(
.broadcast_packet_all(&CEntityAnimation::new( entity_id.into(),
self.entity_id().into(), pumpkin_protocol::java::client::play::Animation::SwingMainArm,
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<dyn EntityBase>) { async fn tick_movement<'a>(&'a self, server: &'a Server, caller: &'a Arc<dyn EntityBase>) {

View File

@ -35,10 +35,19 @@ use pumpkin_nbt::{compound::NbtCompound, tag::NbtTag};
use pumpkin_protocol::java::client::play::{CUpdateEntityPos, CUpdateEntityPosRot}; use pumpkin_protocol::java::client::play::{CUpdateEntityPos, CUpdateEntityPosRot};
use pumpkin_protocol::{ use pumpkin_protocol::{
PositionFlag, PositionFlag,
bedrock::client::set_actor_data::{ 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, CSetActorData, EntityMetadata, MetadataValue, PropertySyncData, entity_data_flag,
entity_data_key, entity_data_key,
}, },
},
codec::var_int::VarInt, codec::var_int::VarInt,
codec::var_ulong::VarULong, codec::var_ulong::VarULong,
java::client::play::{ java::client::play::{
@ -1129,6 +1138,7 @@ impl Entity {
suffocating suffocating
} }
#[expect(clippy::too_many_lines)]
pub fn send_pos_rot(&self) { pub fn send_pos_rot(&self) {
let old = self.last_sent_pos.load(); let old = self.last_sent_pos.load();
let new = self.pos.load(); let new = self.pos.load();
@ -1161,35 +1171,150 @@ impl Entity {
// Dynamically pick the most efficient packet // Dynamically pick the most efficient packet
if pos_changed && rot_changed { if pos_changed && rot_changed {
self.world.load().broadcast_to_chunk( let je_packet = CUpdateEntityPosRot::new(
chunk_pos,
&CUpdateEntityPosRot::new(
self.entity_id.into(), self.entity_id.into(),
Vector3::new(converted.x, converted.y, converted.z), Vector3::new(converted.x, converted.y, converted.z),
yaw, yaw,
pitch, pitch,
self.on_ground.load(Relaxed), 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 { } else if pos_changed {
self.world.load().broadcast_to_chunk( let je_packet = CUpdateEntityPos::new(
chunk_pos,
&CUpdateEntityPos::new(
self.entity_id.into(), self.entity_id.into(),
Vector3::new(converted.x, converted.y, converted.z), Vector3::new(converted.x, converted.y, converted.z),
self.on_ground.load(Relaxed), 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 if rot_changed { } else {
self.world.load().broadcast_to_chunk( 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, chunk_pos,
&CUpdateEntityRot::new( &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 {
let je_packet = CUpdateEntityRot::new(
self.entity_id.into(), self.entity_id.into(),
yaw, yaw,
pitch, pitch,
self.on_ground.load(Relaxed), 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); self.send_head_rot(yaw);
} }
@ -1220,14 +1345,53 @@ impl Entity {
self.last_sent_pos.store(new); self.last_sent_pos.store(new);
self.world.load().broadcast_to_chunk( let je_packet = CUpdateEntityPos::new(
chunk_pos,
&CUpdateEntityPos::new(
self.entity_id.into(), self.entity_id.into(),
Vector3::new(converted.x, converted.y, converted.z), Vector3::new(converted.x, converted.y, converted.z),
self.on_ground.load(Relaxed), 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 // updateWaterState() in yarn

View File

@ -15,12 +15,11 @@ use pumpkin_data::dimension::Dimension;
use pumpkin_data::meta_data_type::MetaDataType; use pumpkin_data::meta_data_type::MetaDataType;
use pumpkin_data::tracked_data::TrackedData; use pumpkin_data::tracked_data::TrackedData;
use pumpkin_inventory::player::ender_chest_inventory::EnderChestInventory; 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::level_chunk::CLevelChunk;
use pumpkin_protocol::bedrock::client::play_status::CPlayStatus; use pumpkin_protocol::bedrock::client::play_status::CPlayStatus;
use pumpkin_protocol::bedrock::client::set_time::CSetTime; use pumpkin_protocol::bedrock::client::set_time::CSetTime;
use pumpkin_protocol::bedrock::client::update_abilities::{ use pumpkin_protocol::bedrock::client::update_abilities::{Ability, CUpdateAbilities};
Ability, AbilityLayer, CUpdateAbilities,
};
use pumpkin_protocol::bedrock::frame_set::FrameSet; use pumpkin_protocol::bedrock::frame_set::FrameSet;
use pumpkin_protocol::bedrock::server::text::SText; use pumpkin_protocol::bedrock::server::text::SText;
use pumpkin_protocol::codec::item_stack_seralizer::ItemStackSerializer; use pumpkin_protocol::codec::item_stack_seralizer::ItemStackSerializer;
@ -493,9 +492,66 @@ pub struct Player {
pub tab_list_listed: AtomicBool, pub tab_list_listed: AtomicBool,
pub enchantment_seed: AtomicI32, pub enchantment_seed: AtomicI32,
pub fishing_bobber: AtomicI32, pub fishing_bobber: AtomicI32,
pub bedrock_skin: arc_swap::ArcSwap<pumpkin_protocol::bedrock::client::Skin>,
}
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<SkinTexture>,
}
#[derive(Deserialize)]
struct SkinTexture {
url: String,
} }
impl Player { impl Player {
#[must_use]
pub fn fetch_skin(properties: &[Property]) -> Option<pumpkin_protocol::bedrock::client::Skin> {
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)] #[expect(clippy::too_many_lines)]
pub async fn new( pub async fn new(
client: ClientPlatform, client: ClientPlatform,
@ -544,6 +600,14 @@ impl Player {
let mut abilities = Abilities::default(); let mut abilities = Abilities::default();
abilities.set_for_gamemode(gamemode); 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 { Self {
living_entity, living_entity,
config: ArcSwap::new(Arc::new(config)), config: ArcSwap::new(Arc::new(config)),
@ -633,6 +697,7 @@ impl Player {
tab_list_latency: AtomicI32::new(0), tab_list_latency: AtomicI32::new(0),
tab_list_listed: AtomicBool::new(false), tab_list_listed: AtomicBool::new(false),
fishing_bobber: AtomicI32::new(-1), 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 /// 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 world = self.world();
let entity_id = VarInt(self.entity_id()); let entity_id = self.entity_id();
let chunk_pos = self.living_entity.entity.chunk_pos.load();
let animation = match hand { let animation = match hand {
Hand::Left => Animation::SwingMainArm, Hand::Right => Animation::SwingMainArm,
Hand::Right => Animation::SwingOffhand, 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 { if all {
world.broadcast_to_chunk(chunk_pos, &packet); world.broadcast_editioned(&je_packet, &be_packet).await;
} else { } 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;
} }
} }

View File

@ -5,8 +5,8 @@ use crate::crash::CrashReport;
use crate::data::VanillaData; use crate::data::VanillaData;
use crate::logging::{GzipRollingLogger, PumpkinCommandCompleter, ReadlineLogWrapper}; use crate::logging::{GzipRollingLogger, PumpkinCommandCompleter, ReadlineLogWrapper};
use crate::net::bedrock::BedrockClient; use crate::net::bedrock::BedrockClient;
use crate::net::java::{JavaClient, PacketHandlerResult}; use crate::net::java::JavaClient;
use crate::net::{ClientPlatform, DisconnectReason}; use crate::net::{ClientPlatform, DisconnectReason, PacketHandlerResult};
use crate::net::{lan_broadcast::LANBroadcast, query, rcon::RCONServer}; use crate::net::{lan_broadcast::LANBroadcast, query, rcon::RCONServer};
use crate::server::{Server, ticker::Ticker}; use crate::server::{Server, ticker::Ticker};
use plugin::server::server_command::ServerCommandEvent; use plugin::server::server_command::ServerCommandEvent;
@ -418,6 +418,7 @@ impl PumpkinServer {
} }
} }
#[allow(clippy::too_many_lines)]
pub async fn unified_listener_task( pub async fn unified_listener_task(
&self, &self,
master_client_id_counter: &mut u64, master_client_id_counter: &mut u64,
@ -501,7 +502,9 @@ impl PumpkinServer {
let be_clients = bedrock_clients.clone(); let be_clients = bedrock_clients.clone();
let mut clients_guard = bedrock_clients.lock().await; let mut clients_guard = bedrock_clients.lock().await;
let mut is_new = false;
let client = clients_guard.entry(client_addr).or_insert_with(|| { let client = clients_guard.entry(client_addr).or_insert_with(|| {
is_new = true;
*master_client_id_counter += 1; *master_client_id_counter += 1;
let new_client = Arc::new(BedrockClient::new( let new_client = Arc::new(BedrockClient::new(
@ -514,6 +517,39 @@ impl PumpkinServer {
new_client new_client
}).clone(); }).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 packet_bytes = udp_buf[..len].to_vec();
let server = self.server.clone(); let server = self.server.clone();

View File

@ -1,5 +1,7 @@
use crate::{ use crate::{
net::{ClientPlatform, DisconnectReason, GameProfile, PlayerConfig, bedrock::BedrockClient}, net::{
DisconnectReason, GameProfile, PacketHandlerResult, PlayerConfig, bedrock::BedrockClient,
},
server::Server, server::Server,
}; };
use arc_swap::ArcSwap; use arc_swap::ArcSwap;
@ -23,7 +25,7 @@ use serde::{Deserialize, de::Error};
use serde_repr::Deserialize_repr; use serde_repr::Deserialize_repr;
use std::sync::Arc; use std::sync::Arc;
use thiserror::Error; use thiserror::Error;
use tracing::{debug, warn}; use tracing::debug;
use uuid::Uuid; use uuid::Uuid;
#[derive(Debug, Error)] #[derive(Debug, Error)]
@ -117,27 +119,19 @@ impl BedrockClient {
self.set_compression(compression).await; self.set_compression(compression).await;
} }
pub async fn handle_login(self: &Arc<Self>, packet: SLogin, server: &Server) -> Option<()> { pub async fn handle_login(
match self.try_handle_login(packet, server).await { self: &Arc<Self>,
Ok(()) => Some(()), packet: SLogin,
Err(error) => { server: &Server,
warn!("Bedrock login failed: {error}"); ) -> Result<PacketHandlerResult, LoginError> {
let message = match error { self.try_handle_login(packet, server).await
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 try_handle_login( pub async fn try_handle_login(
self: &Arc<Self>, self: &Arc<Self>,
packet: SLogin, packet: SLogin,
server: &Server, server: &Server,
) -> Result<(), LoginError> { ) -> Result<PacketHandlerResult, LoginError> {
let auth_payload: AuthPayload = serde_json::from_slice(&packet.jwt)?; let auth_payload: AuthPayload = serde_json::from_slice(&packet.jwt)?;
let player_data = if server.basic_config.online_mode { let player_data = if server.basic_config.online_mode {
match auth_payload.authentication_type { match auth_payload.authentication_type {
@ -222,26 +216,15 @@ impl BedrockClient {
self.send_frame_set(frame_set, 0x84).await; 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 { let new_config = PlayerConfig {
locale: client_data.language_code.clone(), locale: client_data.language_code.clone(),
..Default::default() ..Default::default()
}; };
player.config.store(std::sync::Arc::new(new_config));
self.client_data self.client_data
.store(std::sync::Arc::new(Some(std::sync::Arc::new(client_data)))); .store(std::sync::Arc::new(Some(std::sync::Arc::new(client_data))));
// player spawn happens after resource packs are resolved Ok(PacketHandlerResult::ReadyToPlay(profile, new_config))
*self.player.lock().await = Some(player);
}
Ok(())
} }
pub async fn handle_resource_pack_response( pub async fn handle_resource_pack_response(

View File

@ -57,8 +57,8 @@ use pumpkin_protocol::{
use std::net::SocketAddr; use std::net::SocketAddr;
use tokio::{ use tokio::{
net::UdpSocket, net::UdpSocket,
sync::Mutex,
sync::mpsc::{Receiver, Sender}, sync::mpsc::{Receiver, Sender},
sync::{Mutex, oneshot},
task::JoinHandle, task::JoinHandle,
}; };
use tokio_util::{sync::CancellationToken, task::TaskTracker}; use tokio_util::{sync::CancellationToken, task::TaskTracker};
@ -67,11 +67,36 @@ pub mod connection;
pub mod login; pub mod login;
pub mod open_connection; pub mod open_connection;
pub mod unconnected; 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 arc_swap::ArcSwap;
use pumpkin_protocol::bedrock::server::login::ClientData; use pumpkin_protocol::bedrock::server::login::ClientData;
use pumpkin_util::version::BedrockMinecraftVersion; use pumpkin_util::version::BedrockMinecraftVersion;
pub struct OutgoingPacket {
pub data: Bytes,
pub completion: Option<oneshot::Sender<()>>,
}
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 { pub struct BedrockClient {
socket: Arc<UdpSocket>, socket: Arc<UdpSocket>,
/// The client's IP address. /// The client's IP address.
@ -84,9 +109,12 @@ pub struct BedrockClient {
pub be_clients: Arc<Mutex<HashMap<SocketAddr, Arc<Self>>>>, pub be_clients: Arc<Mutex<HashMap<SocketAddr, Arc<Self>>>>,
tasks: TaskTracker, tasks: TaskTracker,
outgoing_packet_queue_send: Sender<Bytes>, outgoing_packet_queue_send: Sender<OutgoingPacket>,
/// A queue of serialized packets to send to the network /// A queue of serialized packets to send to the network
outgoing_packet_queue_recv: Mutex<Option<Receiver<Bytes>>>, outgoing_packet_queue_recv: Mutex<Option<Receiver<OutgoingPacket>>>,
outgoing_packet_priority_send: Sender<OutgoingPacket>,
outgoing_packet_priority_recv: Mutex<Option<Receiver<OutgoingPacket>>>,
/// The packet encoder for outgoing packets. /// The packet encoder for outgoing packets.
network_writer: Arc<Mutex<UDPNetworkEncoder>>, network_writer: Arc<Mutex<UDPNetworkEncoder>>,
@ -114,6 +142,8 @@ pub struct BedrockClient {
expected_order_index: Mutex<HashMap<u8, u32>>, expected_order_index: Mutex<HashMap<u8, u32>>,
highest_sequence_index: Mutex<HashMap<u8, u32>>, highest_sequence_index: Mutex<HashMap<u8, u32>>,
ordered_queues: Mutex<HashMap<u8, BTreeMap<u32, Frame>>>, ordered_queues: Mutex<HashMap<u8, BTreeMap<u32, Frame>>>,
incoming_game_packet_send: Sender<RawPacket>,
incoming_game_packet_recv: Mutex<Option<Receiver<RawPacket>>>,
} }
impl BedrockClient { impl BedrockClient {
@ -124,6 +154,8 @@ impl BedrockClient {
be_clients: Arc<Mutex<HashMap<SocketAddr, Arc<Self>>>>, be_clients: Arc<Mutex<HashMap<SocketAddr, Arc<Self>>>>,
) -> Self { ) -> Self {
let (send, recv) = tokio::sync::mpsc::channel(128); 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 { Self {
socket, socket,
player: Mutex::new(None), player: Mutex::new(None),
@ -136,6 +168,8 @@ impl BedrockClient {
tasks: TaskTracker::new(), tasks: TaskTracker::new(),
outgoing_packet_queue_send: send, outgoing_packet_queue_send: send,
outgoing_packet_queue_recv: Mutex::new(Some(recv)), 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), _use_frame_sets: AtomicBool::new(false),
output_sequence_number: AtomicU32::new(0), output_sequence_number: AtomicU32::new(0),
output_reliable_number: AtomicU32::new(0), output_reliable_number: AtomicU32::new(0),
@ -153,6 +187,17 @@ impl BedrockClient {
highest_sequence_index: Mutex::new(HashMap::new()), highest_sequence_index: Mutex::new(HashMap::new()),
ordered_queues: Mutex::new(HashMap::new()), ordered_queues: Mutex::new(HashMap::new()),
//input_sequence_number: AtomicU32::new(0), //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<RawPacket> {
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() .take()
.expect("Outgoing packet receiver was already taken") .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)); let mut interval = tokio::time::interval(std::time::Duration::from_millis(100));
while !client.close_token.is_cancelled() { 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() => { _ = interval.tick() => {
// Check for timeout (10 seconds) // Check for timeout (10 seconds)
if client.last_seen.load().elapsed() > std::time::Duration::from_secs(10) { if client.last_seen.load().elapsed() > std::time::Duration::from_secs(10) {
debug!("Bedrock client {} timed out", client.address); debug!("Bedrock client {} timed out", client.address);
client.close().await; client.close().await;
break; return;
} }
// Flush ACKs // Flush ACKs
@ -207,23 +261,21 @@ impl BedrockClient {
warn!("Failed to resend packet for sequence {}: {}", seq, err); warn!("Failed to resend packet for sequence {}: {}", seq, err);
} }
} }
continue;
} }
recv_result = packet_receiver.recv() => { res = packet_receiver.recv() => res,
let Some(packet_data) = recv_result else { };
let Some(packet) = recv_result else {
break; break;
}; };
if let Err(err) = client.network_writer client
.lock() .send_framed_packet_data(packet.data.to_vec(), RakReliability::ReliableOrdered)
.await .await;
.write_packet(&packet_data, client.address, &client.socket)
.await if let Some(completion) = packet.completion {
&& !client.close_token.is_cancelled() { let _ = completion.send(());
warn!("Failed to send packet to client: {err}",);
client.close_token.cancel();
break;
}
}
} }
} }
}); });
@ -259,6 +311,43 @@ impl BedrockClient {
self.close().await; self.close().await;
} }
pub async fn enqueue_packet<P: BClientPacket>(&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<P: BClientPacket>(&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 /// Queues a clientbound packet to be sent to the connected client. Queued chunks are sent
/// in-order to the client /// in-order to the client
/// ///
@ -266,7 +355,11 @@ impl BedrockClient {
/// ///
/// * `packet`: A reference to a packet object implementing the `ClientPacket` trait. /// * `packet`: A reference to a packet object implementing the `ClientPacket` trait.
pub async fn enqueue_packet_data(&self, packet_data: Bytes) { 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 // This is expected to fail if we are closed
if !self.is_closed() { if !self.is_closed() {
error!("Failed to add packet to the outgoing packet queue for client: {err}"); 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) { 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 { match err {
tokio::sync::mpsc::error::TrySendError::Full(_) => { tokio::sync::mpsc::error::TrySendError::Full(_) => {
debug!( debug!(
@ -309,8 +405,8 @@ impl BedrockClient {
let mut packet_payload = Vec::new(); let mut packet_payload = Vec::new();
packet.write_packet(&mut packet_payload)?; packet.write_packet(&mut packet_payload)?;
// TODO let mut encoder = self.network_writer.lock().await;
self.network_writer.lock().await.write_game_packet( encoder.write_game_packet(
P::PACKET_ID as u16, P::PACKET_ID as u16,
SubClient::Main, SubClient::Main,
SubClient::Main, SubClient::Main,
@ -337,8 +433,18 @@ impl BedrockClient {
let mut packet_buf = Vec::new(); let mut packet_buf = Vec::new();
match self.write_game_packet(packet, &mut packet_buf).await { match self.write_game_packet(packet, &mut packet_buf).await {
Ok(()) => { Ok(()) => {
self.send_framed_packet_data(packet_buf, RakReliability::ReliableOrdered) let (tx, rx) = oneshot::channel();
.await; 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}"), Err(err) => error!("Failed to write game packet: {err}"),
} }
@ -710,48 +816,70 @@ impl BedrockClient {
} }
async fn handle_game_packet( async fn handle_game_packet(
self: &Arc<Self>, &self,
server: &Arc<Server>, _server: &Arc<Server>,
packet: RawPacket, packet: RawPacket,
) -> Result<(), Error> { ) -> Result<(), Error> {
let packet_id = packet.id; 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<Self>,
server: &Arc<Server>,
) -> PacketHandlerResult {
while let Some(packet) = self.get_packet().await {
let payload = &mut Cursor::new(&packet.payload); let payload = &mut Cursor::new(&packet.payload);
let result = match packet.id { match packet.id {
SRequestNetworkSettings::PACKET_ID => { SRequestNetworkSettings::PACKET_ID => {
self.handle_request_network_settings( let packet = match SRequestNetworkSettings::read(payload) {
SRequestNetworkSettings::read(payload)?, Ok(p) => p,
server, Err(err) => {
) error!("Failed to read SRequestNetworkSettings: {err}");
.await; continue;
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 {
debug!(
"Received game packet {} before player was initialized",
packet.id
);
Ok(())
}
} }
}; };
result.map_err(|e| Error::new(e.kind(), format!("Game packet {packet_id}: {e}"))) 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 unexpected game packet {} during login sequence",
packet.id
);
}
}
}
PacketHandlerResult::Stop
}
pub async fn progress_player_packets(
self: &Arc<Self>,
player: &Arc<Player>,
server: &Arc<Server>,
) {
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( pub async fn handle_play_packet(
@ -760,8 +888,16 @@ impl BedrockClient {
server: &Arc<Server>, server: &Arc<Server>,
packet: RawPacket, packet: RawPacket,
) -> Result<(), Error> { ) -> Result<(), Error> {
let reader = &mut &packet.payload[..]; let payload = &packet.payload[..];
let reader = &mut &payload[..];
match packet.id { match packet.id {
SClientCacheStatus::PACKET_ID => {
// TODO
}
SResourcePackResponse::PACKET_ID => {
self.handle_resource_pack_response(SResourcePackResponse::read(reader)?, server)
.await;
}
SPlayerAuthInput::PACKET_ID => { SPlayerAuthInput::PACKET_ID => {
self.handle_player_auth_input(player, SPlayerAuthInput::read(reader)?, server) self.handle_player_auth_input(player, SPlayerAuthInput::read(reader)?, server)
.await; .await;
@ -797,7 +933,7 @@ impl BedrockClient {
.await; .await;
} }
SAnimate::PACKET_ID => { 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 => { pumpkin_protocol::bedrock::server::modal_form_response::SModalFormResponse::PACKET_ID => {
self.handle_modal_form_response( self.handle_modal_form_response(

View File

@ -19,8 +19,8 @@ use pumpkin_protocol::{
text::SText, text::SText,
}, },
}, },
codec::{var_int::VarInt, var_long::VarLong}, codec::{var_int::VarInt, var_long::VarLong, var_ulong::VarULong},
java::client::play::{Animation, CSystemChatMessage}, java::client::play::{Animation, CEntityAnimation, CSystemChatMessage},
}; };
use pumpkin_util::{GameMode, math::position::BlockPos, text::TextComponent}; use pumpkin_util::{GameMode, math::position::BlockPos, text::TextComponent};
@ -95,6 +95,7 @@ impl BedrockClient {
player.set_client_loaded(true); player.set_client_loaded(true);
} }
#[expect(clippy::too_many_lines)]
pub async fn handle_player_auth_input( pub async fn handle_player_auth_input(
&self, &self,
player: &Arc<Player>, player: &Arc<Player>,
@ -104,16 +105,135 @@ impl BedrockClient {
if !player.has_client_loaded() { if !player.has_client_loaded() {
return; 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(); let old_pos = player.position();
if new_pos != old_pos { 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); 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; chunker::update_position(player).await;
player.progress_motion(delta).await;
}
} }
let input_data = packet.input_data; let input_data = packet.input_data;
let entity = player.get_entity();
if input_data.get(InputData::StartSprinting as usize) { if input_data.get(InputData::StartSprinting as usize) {
entity.set_sprinting(true).await; entity.set_sprinting(true).await;
@ -189,36 +309,37 @@ impl BedrockClient {
.await; .await;
} }
pub fn handle_animate(&self, player: &Arc<Player>, _server: &Server, packet: &SAnimate) { pub async fn handle_animate(&self, player: &Arc<Player>, _server: &Server, packet: &SAnimate) {
if !player.has_client_loaded() { if !player.has_client_loaded() {
return; return;
} }
let entity = &player.living_entity.entity; 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::SwingArm => Some(Animation::SwingMainArm),
AnimateAction::WakeUp => Some(Animation::LeaveBed), AnimateAction::WakeUp => Some(Animation::LeaveBed),
AnimateAction::CriticalHit => Some(Animation::CriticalEffect), AnimateAction::CriticalHit => Some(Animation::CriticalEffect),
AnimateAction::MagicCriticalHit => Some(Animation::MagicCriticaleffect), AnimateAction::MagicCriticalHit => Some(Animation::MagicCriticaleffect),
_ => None, AnimateAction::StopSleep => None, // TODO
}; };
// if let Some(animation) = java_animation { if let Some(animation) = java_animation {
// let je_packet = CEntityAnimation::new(VarInt(entity.entity_id), animation); let je_packet = CEntityAnimation::new(VarInt(entity.entity_id), animation);
// let be_packet = SAnimate { let be_packet = SAnimate {
// action: packet.action, action: packet.action,
// runtime_entity_id: VarULong(entity.entity_id as u64), runtime_entity_id: VarULong(entity.entity_id as u64),
// boat_rowing_time: packet.boat_rowing_time, data: 0.0,
// }; swing_source: None,
// world.broadcast_editioned(&je_packet, &be_packet).await; };
// } world.broadcast_editioned(&je_packet, &be_packet).await;
}
} }
pub async fn handle_interaction(&self, _player: &Arc<Player>, packet: SInteraction) { pub async fn handle_interaction(&self, player: &Arc<Player>, packet: SInteraction) {
if matches!(packet.action, Action::OpenInventory) { match packet.action {
Action::OpenInventory => {
self.send_game_packet(&CContainerOpen { self.send_game_packet(&CContainerOpen {
container_id: 0, container_id: 0,
container_type: 0xff, container_type: 0xff,
@ -227,6 +348,15 @@ impl BedrockClient {
}) })
.await; .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;
}
}
_ => {}
}
} }
pub async fn handle_container_close(&self, _player: &Arc<Player>, packet: SContainerClose) { pub async fn handle_container_close(&self, _player: &Arc<Player>, packet: SContainerClose) {

View File

@ -67,7 +67,7 @@ pub mod recipe_helper;
pub mod status; pub mod status;
use crate::entity::player::Player; 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::plugin::player::player_custom_payload::PlayerCustomPayloadEvent;
use crate::{error::PumpkinError, net::EncryptionError, server::Server}; use crate::{error::PumpkinError, net::EncryptionError, server::Server};
@ -112,10 +112,9 @@ pub struct JavaClient {
pub last_keep_alive_time: AtomicCell<Instant>, pub last_keep_alive_time: AtomicCell<Instant>,
} }
pub enum PacketHandlerResult { pub enum OutgoingPacketType {
Stop, Normal,
// Signal to spawn the player HighPriority,
ReadyToPlay(GameProfile, PlayerConfig),
} }
struct OutgoingPacket { struct OutgoingPacket {

View File

@ -54,7 +54,9 @@ use pumpkin_inventory::merchant::merchant_screen_handler::MerchantScreenHandler;
use pumpkin_inventory::player::player_inventory::PlayerInventory; use pumpkin_inventory::player::player_inventory::PlayerInventory;
use pumpkin_inventory::screen_handler::{InventoryPlayer, ScreenHandler}; use pumpkin_inventory::screen_handler::{InventoryPlayer, ScreenHandler};
use pumpkin_macros::send_cancellable; use pumpkin_macros::send_cancellable;
use pumpkin_protocol::bedrock::client::CMovePlayer;
use pumpkin_protocol::codec::var_int::VarInt; use pumpkin_protocol::codec::var_int::VarInt;
use pumpkin_protocol::codec::var_ulong::VarULong;
use pumpkin_protocol::java::client::play::{ use pumpkin_protocol::java::client::play::{
CBlockUpdate, CCommandSuggestions, CEntityPositionSync, CHeadRot, COpenSignEditor, CBlockUpdate, CCommandSuggestions, CEntityPositionSync, CHeadRot, COpenSignEditor,
CPingResponse, CPlayerInfoUpdate, CPlayerPosition, CSetSelectedSlot, CSystemChatMessage, CPingResponse, CPlayerInfoUpdate, CPlayerPosition, CSetSelectedSlot, CSystemChatMessage,
@ -309,6 +311,7 @@ impl JavaClient {
true true
} }
#[expect(clippy::too_many_lines)]
pub async fn handle_position( pub async fn handle_position(
&self, &self,
player: &Arc<Player>, player: &Arc<Player>,
@ -369,8 +372,7 @@ impl JavaClient {
// TODO: Warn when player moves to quickly // 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) { 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. // Send the new position to all other players.
world world.broadcast_packet_except_editioned_sync(
.broadcast_packet_except(
&[player.gameprofile.id], &[player.gameprofile.id],
&CUpdateEntityPos::new( &CUpdateEntityPos::new(
player.entity_id().into(), player.entity_id().into(),
@ -381,8 +383,20 @@ impl JavaClient {
), ),
packet.collision & FLAG_ON_GROUND != 0, 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 // Only process fall damage if player is alive
@ -494,8 +508,7 @@ impl JavaClient {
sync_position(player, &world, pos, last_pos, yaw, pitch, (packet.collision & FLAG_ON_GROUND) != 0) sync_position(player, &world, pos, last_pos, yaw, pitch, (packet.collision & FLAG_ON_GROUND) != 0)
{ {
// Send the new position to all other players. // Send the new position to all other players.
world world.broadcast_packet_except_editioned_sync(
.broadcast_packet_except(
&[player.gameprofile.id], &[player.gameprofile.id],
&CUpdateEntityPosRot::new( &CUpdateEntityPosRot::new(
entity_id.into(), entity_id.into(),
@ -508,8 +521,20 @@ impl JavaClient {
pitch as u8, pitch as u8,
(packet.collision & FLAG_ON_GROUND) != 0, (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 world
@ -591,11 +616,37 @@ impl JavaClient {
// let head_yaw = modulus(entity.head_yaw * 256.0 / 360.0, 256.0); // let head_yaw = modulus(entity.head_yaw * 256.0 / 360.0, 256.0);
let world = entity.world.load_full(); let world = entity.world.load_full();
let packet = let je_packet =
CUpdateEntityRot::new(entity_id.into(), yaw as u8, pitch as u8, rotation.ground); 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); let pos = entity.pos.load();
world.broadcast_packet_except(&[player.gameprofile.id], &packet);
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( pub async fn handle_chat_command(
@ -1176,7 +1227,7 @@ impl JavaClient {
server; server;
event; event;
'after: { 'after: {
player.swing_hand(hand, false); player.swing_hand(hand, false).await;
} }
}} }}
} }
@ -2028,7 +2079,7 @@ impl JavaClient {
// TODO: Trigger ANY_BLOCK_USE Criteria // TODO: Trigger ANY_BLOCK_USE Criteria
if matches!(result, BlockActionResult::SuccessServer) { if matches!(result, BlockActionResult::SuccessServer) {
player.swing_hand(hand, true); player.swing_hand(hand, true).await;
} }
return Ok(()); return Ok(());
} }

View File

@ -12,7 +12,7 @@ use std::{
}; };
use pumpkin_data::translation; use pumpkin_data::translation;
use pumpkin_protocol::{ClientPacket, Property}; use pumpkin_protocol::{BClientPacket, ClientPacket, Property};
use pumpkin_util::{ use pumpkin_util::{
Hand, ProfileAction, Hand, ProfileAction,
text::TextComponent, 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 /// This is just a Wrapper for both Java & Bedrock connections
#[expect(clippy::large_enum_variant)] #[expect(clippy::large_enum_variant)]
pub enum ClientPlatform { pub enum ClientPlatform {
@ -190,24 +195,61 @@ impl ClientPlatform {
} }
} }
pub async fn enqueue_packet<P: ClientPacket>(&self, packet: &P) { pub async fn enqueue_packet_editioned<J: ClientPacket, B: BClientPacket>(
&self,
je_packet: &J,
be_packet: &B,
) {
match self { match self {
Self::Java(java) => java.enqueue_packet(packet).await, Self::Java(java) => java.enqueue_packet(je_packet).await,
Self::Bedrock(_) => (), Self::Bedrock(bedrock) => bedrock.enqueue_packet(be_packet).await,
}
}
pub fn try_enqueue_packet_editioned<J: ClientPacket, B: BClientPacket>(
&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<P: ClientPacket>(&self, packet: &P) {
if let Self::Java(java) = self {
java.enqueue_packet(packet).await;
}
}
pub async fn enqueue_be_packet<P: BClientPacket>(&self, packet: &P) {
if let Self::Bedrock(bedrock) = self {
bedrock.enqueue_packet(packet).await;
} }
} }
pub fn try_enqueue_packet<P: ClientPacket>(&self, packet: &P) { pub fn try_enqueue_packet<P: ClientPacket>(&self, packet: &P) {
match self { if let Self::Java(java) = self {
Self::Java(java) => java.try_enqueue_packet(packet), java.try_enqueue_packet(packet);
Self::Bedrock(_) => (), }
}
pub fn try_enqueue_be_packet<P: BClientPacket>(&self, packet: &P) {
if let Self::Bedrock(bedrock) = self {
bedrock.try_enqueue_packet(packet);
} }
} }
pub async fn send_packet_now<P: ClientPacket>(&self, packet: &P) { pub async fn send_packet_now<P: ClientPacket>(&self, packet: &P) {
match self { if let Self::Java(java) = self {
Self::Java(java) => java.send_packet_now(packet).await, java.send_packet_now(packet).await;
Self::Bedrock(_) => (), }
}
pub async fn send_be_packet_now<P: BClientPacket>(&self, packet: &P) {
if let Self::Bedrock(bedrock) = self {
bedrock.send_game_packet(packet).await;
} }
} }

View File

@ -2,7 +2,9 @@ use crate::block::entities::BlockEntity;
use dashmap::DashMap; use dashmap::DashMap;
use pumpkin_data::attributes::Attributes; use pumpkin_data::attributes::Attributes;
use pumpkin_data::chunk::Biome; use pumpkin_data::chunk::Biome;
use pumpkin_protocol::bedrock::client::EntityProperties;
use pumpkin_protocol::bedrock::client::level_event::{CLevelEvent, LevelEvent}; 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_protocol::codec::data_component::data_to_proto_sound;
use pumpkin_world::generation::proto_chunk::GenerationCache; use pumpkin_world::generation::proto_chunk::GenerationCache;
use std::sync::atomic::Ordering::Relaxed; 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::client::start_game::{CStartGame, ServerTelemetryData};
use pumpkin_protocol::bedrock::frame_set::FrameSet; 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::{ use pumpkin_protocol::java::client::play::{
CPlayerSpawnPosition, CRecipeBookAdd, CRecipeBookSettings, CSystemChatMessage, CPlayerSpawnPosition, CRecipeBookAdd, CRecipeBookSettings, CSystemChatMessage,
}; };
@ -81,15 +87,17 @@ use pumpkin_protocol::{
BClientPacket, ClientPacket, IdOr, SoundEvent, BClientPacket, ClientPacket, IdOr, SoundEvent,
bedrock::{ bedrock::{
client::{ client::{
add_player::CAddPlayer,
creative_content::{CreativeContent, Group}, creative_content::{CreativeContent, Group},
gamerules_changed::GameRules, gamerules_changed::GameRules,
player_list::{CPlayerList, PlayerListEntry, Skin},
remove_actor::CRemoveActor,
start_game::{Experiments, GamePublishSetting, LevelSettings}, start_game::{Experiments, GamePublishSetting, LevelSettings},
update_attributes::{Attribute, CUpdateAttributes}, update_attributes::{Attribute, CUpdateAttributes},
}, },
network_item::NetworkItemDescriptor,
server::text::SText, 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::{ java::{
self, self,
client::play::{ client::play::{
@ -105,13 +113,7 @@ use pumpkin_protocol::{
codec::item_stack_seralizer::ItemStackSerializer, codec::item_stack_seralizer::ItemStackSerializer,
java::client::play::{CBlockEvent, CRemoveMobEffect, CSetEquipment, CUpdateMobEffect}, java::client::play::{CBlockEvent, CRemoveMobEffect, CSetEquipment, CUpdateMobEffect},
}; };
use pumpkin_protocol::{ use pumpkin_util::GameMode;
codec::var_int::VarInt,
java::client::play::{
CBlockUpdate, CChunkBatchEnd, CChunkBatchStart, CChunkData, CDisguisedChatMessage,
CExplosion, CRespawn, CSetBlockDestroyStage, CWorldEvent,
},
};
use pumpkin_util::resource_location::ResourceLocation; use pumpkin_util::resource_location::ResourceLocation;
use pumpkin_util::text::{TextComponent, color::NamedColor}; use pumpkin_util::text::{TextComponent, color::NamedColor};
use pumpkin_util::version::JavaMinecraftVersion; use pumpkin_util::version::JavaMinecraftVersion;
@ -618,6 +620,59 @@ impl World {
sender.chat_session.lock().await.messages_sent += 1; sender.chat_session.lock().await.messages_sent += 1;
} }
pub fn broadcast_packet_except_editioned_sync<J: ClientPacket, B: BClientPacket>(
&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<J: ClientPacket, B: BClientPacket>(
&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. /// 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. /// 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 level_info = server.level_info.load();
let weather = self.weather.lock().await; let weather = self.weather.lock().await;
let runtime_id = player.entity_id() as u64; let runtime_id = player.entity_id() as u64;
let (position, yaw, pitch) = if player.has_played_before.load(Ordering::Relaxed) { let (position, yaw, pitch) = if player.has_played_before.load(Ordering::Relaxed) {
let position = player.position(); let position = player.position();
let yaw = player.living_entity.entity.yaw.load(); //info.spawn_angle; 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) (position, level_info.spawn_yaw, level_info.spawn_pitch)
}; };
// Todo make the data less spread // Todo make the data less spread
let level_settings = LevelSettings { let level_settings = LevelSettings {
seed: self.level.seed.0, seed: self.level.seed.0,
@ -1708,17 +1765,12 @@ impl World {
// TODO Make this unique // TODO Make this unique
multiplayer_correlation_id: Uuid::default().to_string(), multiplayer_correlation_id: Uuid::default().to_string(),
enable_itemstack_net_manager: true, 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(), server_version: "Pumpkin Rust Server".to_string(),
compound_id: 10, compound_id: 10,
compound_len: VarUInt(0), compound_len: VarUInt(0),
compound_end: 0, compound_end: 0,
block_registry_checksum: 0, block_registry_checksum: 0,
world_template_id: Uuid::nil(), world_template_id: Uuid::nil(),
// TODO The client needs extra biome data for this
enable_clientside_generation: false, enable_clientside_generation: false,
blocknetwork_ids_are_hashed: false, blocknetwork_ids_are_hashed: false,
server_auth_sounds: false, server_auth_sounds: false,
@ -1731,7 +1783,9 @@ impl World {
}, },
}) })
.await; .await;
chunker::update_position(&player).await; chunker::update_position(&player).await;
client client
.send_game_packet(&CreativeContent { .send_game_packet(&CreativeContent {
groups: &[Group { groups: &[Group {
@ -1747,8 +1801,8 @@ impl World {
let mut abilities = player.abilities.lock().await; let mut abilities = player.abilities.lock().await;
abilities.set_for_gamemode(player.gamemode.load()); 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::WIDTH, MetadataValue::Float(0.6));
metadata.set(entity_data_key::HEIGHT, MetadataValue::Float(1.8)); metadata.set(entity_data_key::HEIGHT, MetadataValue::Float(1.8));
@ -1781,18 +1835,15 @@ impl World {
tick: VarULong(0), tick: VarULong(0),
}; };
client.send_game_packet(&actor_data).await; client.send_game_packet(&actor_data).await;
player.send_abilities_update().await; player.send_abilities_update().await;
{ {
let command_dispatcher = server.command_dispatcher.read().await; let command_dispatcher = server.command_dispatcher.read().await;
client_suggestions::send_bedrock_commands_packet(&player, server, &command_dispatcher) client_suggestions::send_bedrock_commands_packet(&player, server, &command_dispatcher)
.await; .await;
}; };
let mut frame_set = FrameSet::default(); let mut frame_set = FrameSet::default();
// https://github.com/pmmp/PocketMine-MP/blob/0b6d8f8cb2aaa05ffad0b6386bd88d73ef54b395/src/entity/AttributeFactory.php#L34
client client
.write_game_packet_to_set( .write_game_packet_to_set(
&CUpdateAttributes { &CUpdateAttributes {
@ -1865,6 +1916,188 @@ impl World {
) )
.await; .await;
client.send_frame_set(frame_set, 0x84).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)] #[expect(clippy::too_many_lines)]
@ -2093,9 +2326,84 @@ impl World {
let gameprofile = &player.gameprofile; 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. // Spawn the player for every client.
self.broadcast_packet_except( self.broadcast_packet_except_editioned_sync(
&[player.gameprofile.id], &[player.gameprofile.id],
&CSpawnEntity::new( &CSpawnEntity::new(
entity_id.into(), entity_id.into(),
@ -2108,6 +2416,7 @@ impl World {
0.into(), 0.into(),
velocity, velocity,
), ),
&bedrock_add_player,
); );
// Spawn players for our client. // Spawn players for our client.
@ -2121,10 +2430,91 @@ impl World {
let entity = &existing_player.living_entity.entity; let entity = &existing_player.living_entity.entity;
let pos = entity.pos.load(); let pos = entity.pos.load();
let gameprofile = &existing_player.gameprofile; 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 let bedrock_player_list = CPlayerList {
.enqueue_packet(&CSpawnEntity::new( 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(), existing_player.entity_id().into(),
gameprofile.id, gameprofile.id,
i32::from(EntityType::PLAYER.id).into(), i32::from(EntityType::PLAYER.id).into(),
@ -2134,7 +2524,9 @@ impl World {
entity.head_yaw.load(), entity.head_yaw.load(),
0.into(), 0.into(),
entity.velocity.load(), entity.velocity.load(),
)) ),
&bedrock_add_player,
)
.await; .await;
{ {
let config = existing_player.config.load(); let config = existing_player.config.load();
@ -3145,8 +3537,33 @@ impl World {
}); });
if let Some(ref player) = removed_player { if let Some(ref player) = removed_player {
let uuid = player.gameprofile.id; let uuid = player.gameprofile.id;
self.broadcast_packet_all(&CRemovePlayerInfo::new(&[uuid])); let entity_id = player.entity_id();
self.broadcast_packet_all(&CRemoveEntities::new(&[player.entity_id().into()]));
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 { if fire_event {
let msg_comp = TextComponent::translate_cross( let msg_comp = TextComponent::translate_cross(
@ -4273,6 +4690,33 @@ impl World {
Self::broadcast_java_grouped(packet, recipients_by_version); Self::broadcast_java_grouped(packet, recipients_by_version);
} }
pub fn broadcast_to_chunk_editioned_sync<J: ClientPacket, B: BClientPacket>(
&self,
chunk_pos: Vector2<i32>,
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. /// Broadcasts a packet to chunk watchers, excluding specific players.
pub fn broadcast_to_chunk_except<P: ClientPacket>( pub fn broadcast_to_chunk_except<P: ClientPacket>(
&self, &self,