diff --git a/crates/pumpkin-config/src/lib.rs b/crates/pumpkin-config/src/lib.rs index 216282b8d..bbb92e3ad 100644 --- a/crates/pumpkin-config/src/lib.rs +++ b/crates/pumpkin-config/src/lib.rs @@ -36,6 +36,7 @@ pub use networking::bedrock::BedrockConfig; pub use networking::compression::CompressionConfig; pub use networking::java::JavaConfig; pub use networking::lan_broadcast::LANBroadcastConfig; +pub use networking::packet_limiter::PacketLimiterConfig; pub use networking::rcon::RCONConfig; pub use plugins::{PluginOverride, PluginsConfig}; pub use pvp::PVPConfig; @@ -93,6 +94,10 @@ impl LoadConfiguration for PumpkinConfig { }; // Validate Java + assert!( + self.advanced.networking.java.keep_alive_time > 0, + "Java Keep alive time must be greater than 0" + ); assert!( self.advanced.networking.java.view_distance >= min_vd, "Java View distance must be at least 2" diff --git a/crates/pumpkin-config/src/networking/bedrock.rs b/crates/pumpkin-config/src/networking/bedrock.rs index 1e2a5c7ce..3927644c3 100644 --- a/crates/pumpkin-config/src/networking/bedrock.rs +++ b/crates/pumpkin-config/src/networking/bedrock.rs @@ -1,4 +1,4 @@ -use crate::CompressionConfig; +use crate::{CompressionConfig, PacketLimiterConfig}; use serde::{Deserialize, Serialize}; use std::net::{IpAddr, SocketAddr}; use std::num::NonZero; @@ -84,6 +84,8 @@ pub struct BedrockConfig { pub nethernet: NetherNetConfig, /// Whether Bedrock client chunk blob caching is enabled. pub chunk_caching: bool, + /// Packet rate limiting settings. + pub packet_limiter: PacketLimiterConfig, } impl Default for BedrockConfig { @@ -101,6 +103,7 @@ impl Default for BedrockConfig { authentication: BedrockAuthenticationConfig::default(), nethernet: NetherNetConfig::default(), chunk_caching: true, + packet_limiter: PacketLimiterConfig::default(), } } } diff --git a/crates/pumpkin-config/src/networking/java.rs b/crates/pumpkin-config/src/networking/java.rs index 3e40f07ba..1ec786c1c 100644 --- a/crates/pumpkin-config/src/networking/java.rs +++ b/crates/pumpkin-config/src/networking/java.rs @@ -1,4 +1,4 @@ -use crate::{AuthenticationConfig, CompressionConfig}; +use crate::{AuthenticationConfig, CompressionConfig, PacketLimiterConfig}; use serde::{Deserialize, Serialize}; use std::net::SocketAddr; use std::num::NonZero; @@ -21,12 +21,21 @@ pub struct JavaConfig { pub view_distance: NonZero, /// The maximum simulated view distance. pub simulation_distance: NonZero, + /// Time interval in seconds between keep-alive packets sent to Java clients. + #[serde( + alias = "keep-alive-time", + alias = "keep_alive_interval", + alias = "keep-alive-interval" + )] + pub keep_alive_time: u64, /// Java Edition packet compression settings. pub compression: CompressionConfig, /// Message of the Day; the server's description displayed on the status screen. pub motd: String, /// Authentication settings for client connections. pub authentication: AuthenticationConfig, + /// Packet rate limiting settings. + pub packet_limiter: PacketLimiterConfig, } impl Default for JavaConfig { @@ -44,9 +53,49 @@ impl Default for JavaConfig { max_players: 1000, view_distance, simulation_distance, + keep_alive_time: 15, compression: CompressionConfig::default(), motd: "A blazingly fast Pumpkin server!".to_string(), authentication: AuthenticationConfig::default(), + packet_limiter: PacketLimiterConfig::default(), } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn default_keep_alive_time() { + let config = JavaConfig::default(); + assert_eq!(config.keep_alive_time, 15); + } + + #[test] + fn keep_alive_time_deserialization() { + let toml_snake = r" + keep_alive_time = 20 + "; + let config: JavaConfig = toml::from_str(toml_snake).unwrap(); + assert_eq!(config.keep_alive_time, 20); + + let toml_kebab = r" + keep-alive-time = 25 + "; + let config: JavaConfig = toml::from_str(toml_kebab).unwrap(); + assert_eq!(config.keep_alive_time, 25); + + let toml_interval = r" + keep_alive_interval = 30 + "; + let config: JavaConfig = toml::from_str(toml_interval).unwrap(); + assert_eq!(config.keep_alive_time, 30); + + let toml_interval_kebab = r" + keep-alive-interval = 35 + "; + let config: JavaConfig = toml::from_str(toml_interval_kebab).unwrap(); + assert_eq!(config.keep_alive_time, 35); + } +} diff --git a/crates/pumpkin-config/src/networking/mod.rs b/crates/pumpkin-config/src/networking/mod.rs index 6cc18aaa2..7cfb301cd 100644 --- a/crates/pumpkin-config/src/networking/mod.rs +++ b/crates/pumpkin-config/src/networking/mod.rs @@ -24,6 +24,10 @@ pub mod query; /// RCON remote console configuration. pub mod rcon; +/// Packet limiter configuration. +pub mod packet_limiter; +pub use packet_limiter::PacketLimiterConfig; + /// Configuration for server networking features. /// /// Covers authentication, query, RCON, proxying, packet compression, diff --git a/crates/pumpkin-config/src/networking/packet_limiter.rs b/crates/pumpkin-config/src/networking/packet_limiter.rs new file mode 100644 index 000000000..2bf68c11c --- /dev/null +++ b/crates/pumpkin-config/src/networking/packet_limiter.rs @@ -0,0 +1,65 @@ +use serde::{Deserialize, Serialize}; + +/// Configuration for client packet rate limiting. +#[derive(Deserialize, Serialize, Clone, Debug)] +#[serde(default)] +pub struct PacketLimiterConfig { + /// Whether the packet rate limiter is enabled. + pub enabled: bool, + /// Maximum number of incoming packets allowed per second per client. + /// Values <= 0.0 disable the rate limit. + #[serde(alias = "max-packet-rate")] + pub max_packet_rate: f64, + /// Burst allowance capacity for packet rate limiting. + #[serde(alias = "burst-capacity")] + pub burst_capacity: f64, + /// Kick message when a client exceeds the packet rate limit. + #[serde(alias = "kick-message")] + pub kick_message: String, +} + +impl Default for PacketLimiterConfig { + fn default() -> Self { + Self { + enabled: true, + max_packet_rate: 500.0, + burst_capacity: 500.0, + kick_message: "Kicked for spamming packets".to_string(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn toml_parsing_with_kebab_case() { + let toml_str = r#" + enabled = true + max-packet-rate = 300.0 + burst-capacity = 100.0 + kick-message = "Stop spamming" + "#; + let config: PacketLimiterConfig = toml::from_str(toml_str).unwrap(); + assert!(config.enabled); + assert!((config.max_packet_rate - 300.0).abs() < f64::EPSILON); + assert!((config.burst_capacity - 100.0).abs() < f64::EPSILON); + assert_eq!(config.kick_message, "Stop spamming"); + } + + #[test] + fn toml_parsing_with_snake_case() { + let toml_str = r#" + enabled = true + max_packet_rate = 500.0 + burst_capacity = 500.0 + kick_message = "Kicked for spamming packets" + "#; + let config: PacketLimiterConfig = toml::from_str(toml_str).unwrap(); + assert!(config.enabled); + assert!((config.max_packet_rate - 500.0).abs() < f64::EPSILON); + assert!((config.burst_capacity - 500.0).abs() < f64::EPSILON); + assert_eq!(config.kick_message, "Kicked for spamming packets"); + } +} diff --git a/crates/pumpkin-data/src/generated/packet.rs b/crates/pumpkin-data/src/generated/packet.rs index f3dffa682..a8b537765 100644 --- a/crates/pumpkin-data/src/generated/packet.rs +++ b/crates/pumpkin-data/src/generated/packet.rs @@ -190,8 +190,8 @@ pub mod serverbound { v1_21_7: 0i32, v1_21_9: 0i32, v1_21_11: 0i32, - v26_1: -1i32, - v26_2: -1i32, + v26_1: 0i32, + v26_2: 0i32, }; pub const LOGIN_COOKIE_RESPONSE: super::PacketId = super::PacketId { v1_20_5: 4i32, diff --git a/crates/pumpkin-protocol/src/bedrock/packet_encoder.rs b/crates/pumpkin-protocol/src/bedrock/packet_encoder.rs index 1e1bd5c2b..829c56855 100644 --- a/crates/pumpkin-protocol/src/bedrock/packet_encoder.rs +++ b/crates/pumpkin-protocol/src/bedrock/packet_encoder.rs @@ -86,6 +86,44 @@ impl BedrockBatchEncoder { Ok(()) } + + pub fn write_packet( + &self, + packet: &P, + writer: impl Write, + ) -> Result<(), Error> { + let mut packet_payload = Vec::new(); + packet.write_packet(&mut packet_payload)?; + self.write_game_packet( + P::PACKET_ID as u16, + SubClient::Main, + SubClient::Main, + &packet_payload, + writer, + ) + } + + pub fn serialize_packet( + &self, + packet: &P, + ) -> Result { + let mut buf = Vec::new(); + self.write_packet(packet, &mut buf)?; + Ok(buf.into()) + } +} + +pub fn write_packet( + packet: &P, + writer: impl Write, +) -> Result<(), Error> { + BedrockBatchEncoder::new().write_packet(packet, writer) +} + +pub fn serialize_packet( + packet: &P, +) -> Result { + BedrockBatchEncoder::new().serialize_packet(packet) } #[cfg(test)] diff --git a/crates/pumpkin-protocol/src/java/client/config/ping.rs b/crates/pumpkin-protocol/src/java/client/config/ping.rs index df3c3c006..01c01aa55 100644 --- a/crates/pumpkin-protocol/src/java/client/config/ping.rs +++ b/crates/pumpkin-protocol/src/java/client/config/ping.rs @@ -27,3 +27,15 @@ impl ClientPacket for CConfigPing { Ok(()) } } + +impl<'a> crate::ServerPacket<'a> for CConfigPing { + fn read( + read: &mut &'a [u8], + _version: &JavaMinecraftVersion, + ) -> Result { + use crate::ser::NetworkReadExt; + Ok(Self { + id: read.get_i32_be()?, + }) + } +} diff --git a/crates/pumpkin-protocol/src/java/client/play/player_position.rs b/crates/pumpkin-protocol/src/java/client/play/player_position.rs index 8e01f35c5..47348fb00 100644 --- a/crates/pumpkin-protocol/src/java/client/play/player_position.rs +++ b/crates/pumpkin-protocol/src/java/client/play/player_position.rs @@ -91,16 +91,50 @@ impl ClientPacket for CPlayerPosition { impl<'a> ServerPacket<'a> for CPlayerPosition { fn read( read: &mut &'a [u8], - _version: &JavaMinecraftVersion, + version: &JavaMinecraftVersion, ) -> Result { - Ok(Self { - teleport_id: read.get_var_int()?, - // TODO - position: Vector3::new(0.0, 0.0, 0.0), - delta: Vector3::new(0.0, 0.0, 0.0), - yaw: 0.0, - pitch: 0.0, - relatives: Vec::new(), - }) + if version >= &JavaMinecraftVersion::V_1_21_2 { + let teleport_id = read.get_var_int()?; + let x = read.get_f64_be()?; + let y = read.get_f64_be()?; + let z = read.get_f64_be()?; + let dx = read.get_f64_be()?; + let dy = read.get_f64_be()?; + let dz = read.get_f64_be()?; + let yaw = read.get_f32_be()?; + let pitch = read.get_f32_be()?; + let relatives_bits = read.get_i32_be()?; + Ok(Self { + teleport_id, + position: Vector3::new(x, y, z), + delta: Vector3::new(dx, dy, dz), + yaw, + pitch, + relatives: PositionFlag::from_bitfield(relatives_bits), + }) + } else { + let x = read.get_f64_be()?; + let y = read.get_f64_be()?; + let z = read.get_f64_be()?; + let yaw = read.get_f32_be()?; + let pitch = read.get_f32_be()?; + let relatives_bits = i32::from(read.get_u8()?); + let teleport_id = if version >= &JavaMinecraftVersion::V_1_9 { + read.get_var_int()? + } else { + VarInt(0) + }; + if version >= &JavaMinecraftVersion::V_1_20_2 { + let _ = read.get_bool()?; + } + Ok(Self { + teleport_id, + position: Vector3::new(x, y, z), + delta: Vector3::new(0.0, 0.0, 0.0), + yaw, + pitch, + relatives: PositionFlag::from_bitfield(relatives_bits), + }) + } } } diff --git a/crates/pumpkin-protocol/src/java/packet_encoder.rs b/crates/pumpkin-protocol/src/java/packet_encoder.rs index a8ea3140c..7347789b1 100644 --- a/crates/pumpkin-protocol/src/java/packet_encoder.rs +++ b/crates/pumpkin-protocol/src/java/packet_encoder.rs @@ -1,12 +1,14 @@ use aes::cipher::KeyIvInit; use bytes::Bytes; use flate2::{Compress, Compression, FlushCompress, Status}; +use pumpkin_util::version::JavaMinecraftVersion; use thiserror::Error; use tokio::io::{AsyncWrite, AsyncWriteExt}; use crate::{ - Aes128Cfb8Enc, CompressionLevel, CompressionThreshold, MAX_PACKET_DATA_SIZE, MAX_PACKET_SIZE, - PacketEncodeError, StreamEncryptor, VarInt, + Aes128Cfb8Enc, ClientPacket, CompressionLevel, CompressionThreshold, MAX_PACKET_DATA_SIZE, + MAX_PACKET_SIZE, PacketEncodeError, StreamEncryptor, VarInt, WritingError, + ser::NetworkWriteExt, }; // raw -> compress -> encrypt @@ -318,6 +320,28 @@ impl TCPNetworkEncoder { } } +pub fn write_packet( + packet: &P, + version: &JavaMinecraftVersion, + mut write: impl std::io::Write, +) -> Result<(), WritingError> { + let version_number = P::to_id(*version); + if version_number == -1 { + return Ok(()); + } + write.write_var_int(&VarInt(version_number))?; + packet.write_packet_data(write, version) +} + +pub fn serialize_packet( + packet: &P, + version: &JavaMinecraftVersion, +) -> Result { + let mut packet_buf = Vec::new(); + write_packet(packet, version, &mut packet_buf)?; + Ok(packet_buf.into()) +} + #[derive(Error, Debug)] #[error("Invalid compression Level")] pub struct CompressionLevelError; diff --git a/crates/pumpkin-protocol/src/lib.rs b/crates/pumpkin-protocol/src/lib.rs index c6c9f071e..729b03c67 100644 --- a/crates/pumpkin-protocol/src/lib.rs +++ b/crates/pumpkin-protocol/src/lib.rs @@ -255,6 +255,18 @@ pub trait ClientPacket: MultiVersionJavaPacket { write: impl Write, version: &JavaMinecraftVersion, ) -> Result<(), WritingError>; + + fn write_packet( + &self, + version: &JavaMinecraftVersion, + write: impl Write, + ) -> Result<(), WritingError> { + crate::java::packet_encoder::write_packet(self, version, write) + } + + fn serialize_packet(&self, version: &JavaMinecraftVersion) -> Result { + crate::java::packet_encoder::serialize_packet(self, version) + } } pub trait ServerPacket<'a>: MultiVersionJavaPacket + Sized { @@ -263,6 +275,10 @@ pub trait ServerPacket<'a>: MultiVersionJavaPacket + Sized { pub trait BClientPacket: Packet { fn write_packet(&self, writer: impl Write) -> Result<(), Error>; + + fn serialize_packet(&self) -> Result { + crate::bedrock::packet_encoder::serialize_packet(self) + } } pub trait BServerPacket: Packet + Sized { @@ -447,6 +463,24 @@ impl PositionFlag { pub fn get_bitfield(flags: &[Self]) -> i32 { flags.iter().fold(0, |acc, flag| acc | flag.get_mask()) } + + #[must_use] + pub fn from_bitfield(bits: i32) -> Vec { + let all = [ + Self::X, + Self::Y, + Self::Z, + Self::YRot, + Self::XRot, + Self::DeltaX, + Self::DeltaY, + Self::DeltaZ, + Self::RotateDelta, + ]; + all.into_iter() + .filter(|flag| (bits & flag.get_mask()) != 0) + .collect() + } } pub enum Label { diff --git a/crates/pumpkin-world/src/chunk_system/schedule.rs b/crates/pumpkin-world/src/chunk_system/schedule.rs index c06b01fa9..1927dc973 100644 --- a/crates/pumpkin-world/src/chunk_system/schedule.rs +++ b/crates/pumpkin-world/src/chunk_system/schedule.rs @@ -1282,6 +1282,12 @@ impl GenerationSchedule { }); if node.stage > effective_target { + if let Some(holder) = self.chunk_map.get_mut(&node.pos) { + let task_slot = &mut holder.tasks[node.stage as usize]; + if *task_slot == task.1 { + *task_slot = NodeKey::null(); + } + } self.waiting_for_chunks.remove(&task.1); self.drop_node(task.1); continue; diff --git a/crates/pumpkin-world/src/chunk_system/tests.rs b/crates/pumpkin-world/src/chunk_system/tests.rs index dc636e0d9..464aeba49 100644 --- a/crates/pumpkin-world/src/chunk_system/tests.rs +++ b/crates/pumpkin-world/src/chunk_system/tests.rs @@ -416,3 +416,47 @@ fn dag_prune_edge_chain_removes_dead_target_edges() { assert!(head.is_null()); assert_eq!(graph.edges.len(), 0); } + +#[test] +fn cancelled_out_of_range_task_clears_holder_task_slot() { + let mut graph = DAG::default(); + let mut chunk_map = HashMap::new(); + let pos = ChunkPos::new(0, 0); + + let stage = StagedChunkEnum::Biomes; + let node_key = graph.nodes.insert(Node::new(pos, stage)); + + let mut holder = ChunkHolder { + current_stage: StagedChunkEnum::Empty, + target_stage: StagedChunkEnum::None, // Chunk was downgraded / moved out of range + dependency_stage: StagedChunkEnum::None, + ..Default::default() + }; + holder.tasks[stage as usize] = node_key; + chunk_map.insert(pos, holder); + + // Simulate task processing loop cancellation when node.stage > effective_target + let node = graph.nodes.get(node_key).unwrap().clone(); + let effective_target = chunk_map.get(&node.pos).map_or(StagedChunkEnum::None, |h| { + h.target_stage.max(h.dependency_stage) + }); + + assert!(node.stage > effective_target); + if let Some(holder) = chunk_map.get_mut(&node.pos) { + let task_slot = &mut holder.tasks[node.stage as usize]; + if *task_slot == node_key { + *task_slot = NodeKey::null(); + } + } + graph.fast_drop_node(node_key); + + let holder = chunk_map.get(&pos).unwrap(); + assert!( + holder.tasks[stage as usize].is_null(), + "Task slot must be null after cancellation" + ); + assert!( + holder.tasks.iter().all(Key::is_null), + "All task slots must be null when idle" + ); +} diff --git a/crates/pumpkin/src/command/client_suggestions.rs b/crates/pumpkin/src/command/client_suggestions.rs index 29050e4f2..7e012eb50 100644 --- a/crates/pumpkin/src/command/client_suggestions.rs +++ b/crates/pumpkin/src/command/client_suggestions.rs @@ -208,7 +208,7 @@ pub async fn send_c_commands_packet( } let packet = CCommands::new(proto_nodes.into(), VarInt(root_node_index as i32)); - player.client.enqueue_packet(&packet).await; + player.send_client_packet(&packet).await; } fn resolve_node_id(node_id: NodeId, node_id_offset: usize, root_node_index: usize) -> usize { @@ -435,7 +435,7 @@ pub async fn send_bedrock_commands_packet( }; if let crate::net::ClientPlatform::Bedrock(bedrock_client) = player.client.as_ref() { - bedrock_client.send_game_packet(&packet).await; + bedrock_client.send_packet(&packet).await; } } diff --git a/crates/pumpkin/src/command/commands/dialog.rs b/crates/pumpkin/src/command/commands/dialog.rs index b145139e8..ea85643c5 100644 --- a/crates/pumpkin/src/command/commands/dialog.rs +++ b/crates/pumpkin/src/command/commands/dialog.rs @@ -28,7 +28,7 @@ impl CommandExecutor for DialogClearExecutor { let count = targets.len(); let packet = CPlayClearDialog::new(); for player in &targets { - player.client.send_packet_now(&packet).await; + player.send_client_packet(&packet).await; } let msg = if count == 1 { @@ -65,7 +65,7 @@ impl CommandExecutor for DialogShowExecutor { let packet = CPlayShowDialog::new(IdOr::Value(dialog_nbt)); for player in &targets { - player.client.send_packet_now(&packet).await; + player.send_client_packet(&packet).await; } let msg = if count == 1 { diff --git a/crates/pumpkin/src/command/commands/recipe.rs b/crates/pumpkin/src/command/commands/recipe.rs index 399bf7374..a8c37c33b 100644 --- a/crates/pumpkin/src/command/commands/recipe.rs +++ b/crates/pumpkin/src/command/commands/recipe.rs @@ -108,13 +108,10 @@ impl CommandExecutor for RecipeGiveExecutor { } let recipe_count = matching_recipes.len(); + let packet = CRecipeBookAdd::new(false, &matching_recipes); for player in &targets { - if let crate::net::ClientPlatform::Java(java_client) = player.client.as_ref() { - java_client - .send_packet_now(&CRecipeBookAdd::new(false, &matching_recipes)) - .await; - } + player.send_client_packet(&packet).await; } let recipe_count_str = recipe_count.to_string(); @@ -193,12 +190,9 @@ impl CommandExecutor for RecipeTakeExecutor { all_recipes.len() - remaining_recipes.len() }; + let packet = CRecipeBookAdd::new(true, &remaining_recipes); for player in &targets { - if let crate::net::ClientPlatform::Java(java_client) = player.client.as_ref() { - java_client - .send_packet_now(&CRecipeBookAdd::new(true, &remaining_recipes)) - .await; - } + player.send_client_packet(&packet).await; } let taken_count_str = taken_count.to_string(); diff --git a/crates/pumpkin/src/command/commands/spectate.rs b/crates/pumpkin/src/command/commands/spectate.rs index 71af48ddc..784298e38 100644 --- a/crates/pumpkin/src/command/commands/spectate.rs +++ b/crates/pumpkin/src/command/commands/spectate.rs @@ -45,8 +45,7 @@ impl CommandExecutor for StopSpectateExecutor { player.camera_target_id.store(None); player - .client - .send_packet_now(&CSetCamera::new(player.entity_id().into())) + .send_client_packet(&CSetCamera::new(player.entity_id().into())) .await; sender @@ -110,8 +109,7 @@ impl CommandExecutor for SpectateTargetSelfExecutor { let target_id = target_entity.entity_id; player.camera_target_id.store(Some(target_id)); player - .client - .send_packet_now(&CSetCamera::new(target_id.into())) + .send_client_packet(&CSetCamera::new(target_id.into())) .await; let pos = target_entity.pos.load(); @@ -189,8 +187,7 @@ impl CommandExecutor for SpectateTargetOtherExecutor { let target_id = target_entity.entity_id; player.camera_target_id.store(Some(target_id)); player - .client - .send_packet_now(&CSetCamera::new(target_id.into())) + .send_client_packet(&CSetCamera::new(target_id.into())) .await; let pos = target_entity.pos.load(); diff --git a/crates/pumpkin/src/command/commands/title.rs b/crates/pumpkin/src/command/commands/title.rs index 02084395a..c8365e38c 100644 --- a/crates/pumpkin/src/command/commands/title.rs +++ b/crates/pumpkin/src/command/commands/title.rs @@ -44,7 +44,7 @@ impl CommandExecutor for ClearOrResetExecutor { let reset = self.0; for target in targets { - target.client.enqueue_packet(&CClearTitle::new(reset)).await; + target.send_client_packet(&CClearTitle::new(reset)).await; } sender .send_message(if targets.len() == 1 { diff --git a/crates/pumpkin/src/command/commands/transfer.rs b/crates/pumpkin/src/command/commands/transfer.rs index df188c91e..8022d35d9 100644 --- a/crates/pumpkin/src/command/commands/transfer.rs +++ b/crates/pumpkin/src/command/commands/transfer.rs @@ -13,7 +13,6 @@ use crate::command::dispatcher::CommandError::{self, InvalidConsumption, Invalid use crate::command::tree::builder::{argument, argument_default_name, require}; use crate::command::{CommandExecutor, CommandSender, args::ConsumedArgs, tree::CommandTree}; use crate::entity::EntityBase; -use crate::net::ClientPlatform; const NAMES: [&str; 1] = ["transfer"]; @@ -58,22 +57,12 @@ impl CommandExecutor for TargetSelfExecutor { let name = &player.gameprofile.name; info!("[{name}: Transferring {name} to {hostname}:{port}]"); - match player.client.as_ref() { - ClientPlatform::Java(client) => { - client - .enqueue_packet(&JavaCTransfer::new(hostname, VarInt(port))) - .await; - } - ClientPlatform::Bedrock(client) => { - client - .send_game_packet(&BedrockCTransfer::new( - hostname.to_string(), - port as u16, - false, - )) - .await; - } - } + player + .enqueue_packet_editioned( + &JavaCTransfer::new(hostname, VarInt(port)), + &BedrockCTransfer::new(hostname.to_string(), port as u16, false), + ) + .await; Ok(1) } else { @@ -121,22 +110,11 @@ impl CommandExecutor for TargetPlayerExecutor { } for p in players { - match p.client.as_ref() { - ClientPlatform::Java(client) => { - client - .enqueue_packet(&JavaCTransfer::new(hostname, VarInt(port))) - .await; - } - ClientPlatform::Bedrock(client) => { - client - .send_game_packet(&BedrockCTransfer::new( - hostname.to_string(), - port as u16, - false, - )) - .await; - } - } + p.enqueue_packet_editioned( + &JavaCTransfer::new(hostname, VarInt(port)), + &BedrockCTransfer::new(hostname.to_string(), port as u16, false), + ) + .await; info!( "[{sender}: Transferring {} to {hostname}:{port}]", diff --git a/crates/pumpkin/src/command/commands/waypoint.rs b/crates/pumpkin/src/command/commands/waypoint.rs index 3f85da768..3bfbe8d93 100644 --- a/crates/pumpkin/src/command/commands/waypoint.rs +++ b/crates/pumpkin/src/command/commands/waypoint.rs @@ -90,7 +90,7 @@ impl CommandExecutor for ColorExecutor { ); if let Some(player) = sender.as_player() { - player.client.enqueue_packet(&packet).await; + player.send_client_packet(&packet).await; } match self.0 { @@ -173,7 +173,7 @@ impl CommandExecutor for StyleExecutor { ); if let Some(player) = sender.as_player() { - player.client.enqueue_packet(&packet).await; + player.send_client_packet(&packet).await; } sender diff --git a/crates/pumpkin/src/entity/decoration/item_frame.rs b/crates/pumpkin/src/entity/decoration/item_frame.rs index 1572d5f46..16a392f1b 100644 --- a/crates/pumpkin/src/entity/decoration/item_frame.rs +++ b/crates/pumpkin/src/entity/decoration/item_frame.rs @@ -348,9 +348,10 @@ impl EntityBase for ItemFrameEntity { client: &'a crate::net::java::JavaClient, ) -> EntityBaseFuture<'a, ()> { Box::pin(async move { - client - .enqueue_packet(&self.entity.create_spawn_packet()) - .await; + let spawn_packet = self.entity.create_spawn_packet(); + if let Ok(data) = client.serialize_packet(&spawn_packet) { + client.enqueue_packet(data).await; + } let ver = client.version.load(); if ver >= CURRENT_MC_VERSION { @@ -370,12 +371,11 @@ impl EntityBase for ItemFrameEntity { && meta_rot.write(&mut data, &ver).is_ok() { data.push(255); - client - .enqueue_packet(&CSetEntityMetadata::new( - self.entity.entity_id.into(), - data.into(), - )) - .await; + let meta_packet = + CSetEntityMetadata::new(self.entity.entity_id.into(), data.into()); + if let Ok(meta_data) = client.serialize_packet(&meta_packet) { + client.enqueue_packet(meta_data).await; + } } } }) diff --git a/crates/pumpkin/src/entity/item.rs b/crates/pumpkin/src/entity/item.rs index adbdb5a5d..1e73ad252 100644 --- a/crates/pumpkin/src/entity/item.rs +++ b/crates/pumpkin/src/entity/item.rs @@ -656,7 +656,9 @@ impl EntityBase for ItemEntity { metadata: entity.bedrock_metadata(), from_fishing: false, }; - client.send_game_packet(&packet).await; + if let Ok(data) = client.serialize_packet(&packet) { + client.send_game_packet(data).await; + } }) } @@ -665,9 +667,10 @@ impl EntityBase for ItemEntity { client: &'a crate::net::java::JavaClient, ) -> EntityBaseFuture<'a, ()> { Box::pin(async move { - client - .enqueue_packet(&self.entity.create_spawn_packet()) - .await; + let spawn_packet = self.entity.create_spawn_packet(); + if let Ok(data) = client.serialize_packet(&spawn_packet) { + client.enqueue_packet(data).await; + } if client.version.load() >= CURRENT_MC_VERSION { let metadata = Metadata::new( @@ -677,12 +680,11 @@ impl EntityBase for ItemEntity { let mut data = Vec::new(); if metadata.write(&mut data, &client.version.load()).is_ok() { data.push(255); - client - .enqueue_packet(&CSetEntityMetadata::new( - self.entity.entity_id.into(), - data.into(), - )) - .await; + let meta_packet = + CSetEntityMetadata::new(self.entity.entity_id.into(), data.into()); + if let Ok(meta_data) = client.serialize_packet(&meta_packet) { + client.enqueue_packet(meta_data).await; + } } } }) diff --git a/crates/pumpkin/src/entity/mod.rs b/crates/pumpkin/src/entity/mod.rs index 957047adf..db9f3cd5e 100644 --- a/crates/pumpkin/src/entity/mod.rs +++ b/crates/pumpkin/src/entity/mod.rs @@ -333,24 +333,26 @@ pub trait EntityBase: Send + Sync + NBTStorage + std::any::Any { }, Vec::new(), ); - client.send_game_packet(&packet).await; + if let Ok(data) = client.serialize_packet(&packet) { + client.send_game_packet(data).await; + } }) } fn send_java_spawn_packet<'a>(&'a self, client: &'a JavaClient) -> EntityBaseFuture<'a, ()> { Box::pin(async move { - client - .enqueue_packet(&self.get_entity().create_spawn_packet()) - .await; + let spawn_packet = self.get_entity().create_spawn_packet(); + if let Ok(data) = client.serialize_packet(&spawn_packet) { + client.enqueue_packet(data).await; + } if let Some(mob) = self.get_mob() && let Some(metadata) = mob.mob_java_spawn_metadata(client.version.load()).await { - client - .enqueue_packet(&CSetEntityMetadata::new( - self.get_entity().entity_id.into(), - metadata, - )) - .await; + let meta_packet = + CSetEntityMetadata::new(self.get_entity().entity_id.into(), metadata); + if let Ok(meta_data) = client.serialize_packet(&meta_packet) { + client.enqueue_packet(meta_data).await; + } } }) } @@ -1862,15 +1864,7 @@ impl Entity { 0, ); let world = self.world.load(); - for player in world.players.load().iter() { - let center = player.get_entity().chunk_pos.load(); - let view_distance = crate::world::chunker::get_view_distance(player).get() as i32; - if is_within_view_distance(chunk_pos, center, view_distance) - && let ClientPlatform::Bedrock(client) = player.client.as_ref() - { - client.try_enqueue_packet(&packet); - } - } + world.broadcast_to_chunk_bedrock(chunk_pos, &packet); } pub fn update_last_pos(&self) -> Vector3 { @@ -2684,10 +2678,11 @@ impl Entity { )); } + #[expect(clippy::unused_async)] pub async fn set_sneaking(&self, sneaking: bool) { //assert!(self.sneaking.load(Relaxed) != sneaking); self.sneaking.store(sneaking, Relaxed); - self.set_flag(Flag::Sneaking, sneaking).await; + self.set_flag(Flag::Sneaking, sneaking); } pub fn is_sneaking(&self) -> bool { self.sneaking.load(Ordering::Relaxed) @@ -2707,31 +2702,34 @@ impl Entity { return; } self.swimming.store(event.is_swimming, Relaxed); - self.set_flag(Flag::Swimming, event.is_swimming).await; + self.set_flag(Flag::Swimming, event.is_swimming); } } /// Sets whether the entity is invisible and sends updated metadata. + #[expect(clippy::unused_async)] pub async fn set_invisible(&self, invisible: bool) { if self.invisible.load(Ordering::Relaxed) != invisible { self.invisible.store(invisible, Relaxed); - self.set_flag(Flag::Invisible, invisible).await; + self.set_flag(Flag::Invisible, invisible); } } /// Sets whether the entity is glowing and sends updated metadata. + #[expect(clippy::unused_async)] pub async fn set_glowing(&self, glowing: bool) { if self.glowing.load(Ordering::Relaxed) != glowing { self.glowing.store(glowing, Ordering::Relaxed); - self.set_flag(Flag::Glowing, glowing).await; + self.set_flag(Flag::Glowing, glowing); } } /// Sets whether the entity is on fire for visual and damage purposes. This is separate from `fire_ticks` which tracks the damage aspect of being on fire. + #[expect(clippy::unused_async)] pub async fn set_on_fire(&self, on_fire: bool) { if self.has_visual_fire.load(Ordering::Relaxed) != on_fire { self.has_visual_fire.store(on_fire, Ordering::Relaxed); - self.set_flag(Flag::OnFire, on_fire).await; + self.set_flag(Flag::OnFire, on_fire); } } @@ -2840,10 +2838,11 @@ impl Entity { ] } + #[expect(clippy::unused_async)] pub async fn set_sprinting(&self, sprinting: bool) { //assert!(self.sprinting.load(Relaxed) != sprinting); self.sprinting.store(sprinting, Relaxed); - self.set_flag(Flag::Sprinting, sprinting).await; + self.set_flag(Flag::Sprinting, sprinting); } pub fn is_sprinting(&self) -> bool { @@ -2853,16 +2852,17 @@ impl Entity { !self.on_ground.load(Relaxed) } + #[expect(clippy::unused_async)] pub async fn set_fall_flying(&self, fall_flying: bool) { assert_ne!(self.fall_flying.load(Relaxed), fall_flying); self.fall_flying.store(fall_flying, Relaxed); - self.set_flag(Flag::FallFlying, fall_flying).await; + self.set_flag(Flag::FallFlying, fall_flying); } pub fn is_fall_flying(&self) -> bool { self.fall_flying.load(Ordering::Relaxed) } - async fn set_flag(&self, flag: Flag, value: bool) { + fn set_flag(&self, flag: Flag, value: bool) { let index = flag as u8; let mask = (1i8).wrapping_shl(index as u32); let new_je_flags = if value { @@ -2904,36 +2904,25 @@ impl Entity { let world = self.world.load(); let chunk_pos = self.chunk_pos.load(); - for player in world.players.load().iter() { - if let ClientPlatform::Bedrock(client) = player.client.as_ref() { - let center = player.get_entity().chunk_pos.load(); - let view_distance = - crate::world::chunker::get_view_distance(player).get() as i32; - - if is_within_view_distance(chunk_pos, center, view_distance) { - let mut metadata = EntityMetadata(std::collections::HashMap::new()); - metadata.set( - entity_data_key::FLAGS, - MetadataValue::Long(self.bedrock_flags.load(Ordering::Relaxed)), - ); - metadata.set( - entity_data_key::FLAGS_TWO, - MetadataValue::Long(self.bedrock_flags_two.load(Ordering::Relaxed)), - ); - client - .enqueue_packet(&CSetActorData { - actor_runtime_id: VarULong(self.entity_id as u64), - metadata, - synced_properties: PropertySyncData { - int_properties: std::collections::HashMap::new(), - float_properties: std::collections::HashMap::new(), - }, - tick: VarULong(0), - }) - .await; - } - } - } + let mut metadata = EntityMetadata(std::collections::HashMap::new()); + metadata.set( + entity_data_key::FLAGS, + MetadataValue::Long(self.bedrock_flags.load(Ordering::Relaxed)), + ); + metadata.set( + entity_data_key::FLAGS_TWO, + MetadataValue::Long(self.bedrock_flags_two.load(Ordering::Relaxed)), + ); + let packet = CSetActorData { + actor_runtime_id: VarULong(self.entity_id as u64), + metadata, + synced_properties: PropertySyncData { + int_properties: std::collections::HashMap::new(), + float_properties: std::collections::HashMap::new(), + }, + tick: VarULong(0), + }; + world.broadcast_to_chunk_bedrock(chunk_pos, &packet); } } @@ -2951,49 +2940,58 @@ impl Entity { ) { let world = self.world.load(); let chunk_pos = self.chunk_pos.load(); + let players = world.players.load(); - for player in world.players.load().iter() { - match player.client.as_ref() { - ClientPlatform::Java(client) => { - let version = client.version.load(); - if version < CURRENT_MC_VERSION { - continue; - } - // Apply Chebyshev distance check - let center = player.get_entity().chunk_pos.load(); - let view_distance = - crate::world::chunker::get_view_distance(player).get() as i32; + let mut java_recipients = Vec::new(); + let mut bedrock_recipients = Vec::new(); - if is_within_view_distance(chunk_pos, center, view_distance) { - let mut buf = Vec::new(); - for m in meta { - let _ = m.write(&mut buf, &version); - } - buf.put_u8(255); - player.client.try_enqueue_packet(&CSetEntityMetadata::new( - self.entity_id.into(), - buf.into(), - )); - } + for player in players.iter() { + let center = player.get_entity().chunk_pos.load(); + let view_distance = crate::world::chunker::get_view_distance(player).get() as i32; + + if is_within_view_distance(chunk_pos, center, view_distance) { + match player.client.as_ref() { + ClientPlatform::Java(_) => java_recipients.push(player), + ClientPlatform::Bedrock(client) => bedrock_recipients.push(client), } - ClientPlatform::Bedrock(client) => { - if let Some(bedrock_meta) = bedrock_meta { - let center = player.get_entity().chunk_pos.load(); - let view_distance = - crate::world::chunker::get_view_distance(player).get() as i32; + } + } - if is_within_view_distance(chunk_pos, center, view_distance) { - client.try_enqueue_packet(&CSetActorData { - actor_runtime_id: VarULong(self.entity_id as u64), - metadata: EntityMetadata(bedrock_meta.0.clone()), - synced_properties: PropertySyncData { - int_properties: std::collections::HashMap::new(), - float_properties: std::collections::HashMap::new(), - }, - tick: VarULong(0), - }); - } - } + let recipients_by_version = + World::collect_java_recipients_by_version(java_recipients.into_iter()); + + for (version, recipients) in recipients_by_version { + if version < CURRENT_MC_VERSION { + continue; + } + let mut buf = Vec::new(); + for m in meta { + let _ = m.write(&mut buf, &version); + } + buf.put_u8(255); + let packet = CSetEntityMetadata::new(self.entity_id.into(), buf.into()); + if let Ok(packet_data) = JavaClient::serialize_packet_for_version(&packet, version) { + for recipient in recipients { + recipient.try_enqueue_packet(packet_data.clone()); + } + } + } + + if let Some(bedrock_meta) = bedrock_meta { + let packet = CSetActorData { + actor_runtime_id: VarULong(self.entity_id as u64), + metadata: EntityMetadata(bedrock_meta.0.clone()), + synced_properties: PropertySyncData { + int_properties: std::collections::HashMap::new(), + float_properties: std::collections::HashMap::new(), + }, + tick: VarULong(0), + }; + if let Ok(packet_data) = + pumpkin_protocol::bedrock::packet_encoder::serialize_packet(&packet) + { + for recipient in bedrock_recipients { + recipient.try_enqueue_packet(packet_data.clone()); } } } @@ -3447,7 +3445,7 @@ impl Entity { let world = self.world.load(); let passengers_packet = CSetPassengers::new(VarInt(self.entity_id), &passenger_ids); if let Some(player) = passenger.get_player() { - player.client.enqueue_packet(&passengers_packet).await; + player.send_client_packet(&passengers_packet).await; world.broadcast_to_chunk_except( chunk_pos, &[player.get_entity().entity_uuid], @@ -3628,18 +3626,21 @@ impl Entity { } }; + // Clean up any remaining reference to the dismounted passenger. + passenger_entity.set_pos(dismount_pos); + + // Phase 2: Teleport to safety (unblocks movement) if let Some(player) = passenger.get_player() { if let Some(id) = teleport_id { player.get_entity().set_pos(dismount_pos); // Update awaiting_teleport with the real dismount position *player.awaiting_teleport.lock().await = Some((id.into(), dismount_pos)); - // Use enqueue_packet (not send_packet_now) so the teleport goes through + // Use send_client_packet so the teleport goes through // the same packet queue as CSetPassengers, preserving send order. // Vanilla uses DELTA | ROT flags: position absolute, delta/rotation relative. // With rotation relative and yaw/pitch=0, the client preserves its current look. player - .client - .enqueue_packet(&CPlayerPosition::new( + .send_client_packet(&CPlayerPosition::new( id.into(), dismount_pos, Vector3::new(0.0, 0.0, 0.0), diff --git a/crates/pumpkin/src/entity/player.rs b/crates/pumpkin/src/entity/player.rs index 44acc7bdc..100c62373 100644 --- a/crates/pumpkin/src/entity/player.rs +++ b/crates/pumpkin/src/entity/player.rs @@ -62,16 +62,16 @@ pub struct JavaPlayer<'a>(pub &'a Player); impl JavaPlayer<'_> { pub async fn send_packet(&self, packet: &C) { - if let ClientPlatform::Java(client) = self.0.client.as_ref() { - client.enqueue_packet(packet).await; + if let ClientPlatform::Java(client) = self.0.client.as_ref() + && let Ok(data) = client.serialize_packet(packet) + { + client.enqueue_packet(data).await; } } pub async fn send_custom_payload(&self, channel: &str, data: &[u8]) { - if let ClientPlatform::Java(java) = self.0.client.as_ref() { - java.enqueue_packet(&CCustomPayload::new(channel, data)) - .await; - } + let packet = CCustomPayload::new(channel, data); + self.send_packet(&packet).await; } pub async fn send_stats(&self) { @@ -102,8 +102,10 @@ pub struct BedrockPlayer<'a>(pub &'a Player); impl BedrockPlayer<'_> { pub async fn send_packet(&self, packet: &P) { - if let ClientPlatform::Bedrock(client) = self.0.client.as_ref() { - client.send_game_packet(packet).await; + if let ClientPlatform::Bedrock(client) = self.0.client.as_ref() + && let Ok(data) = client.serialize_packet(packet) + { + client.enqueue_packet(data).await; } } @@ -1070,8 +1072,7 @@ impl Player { pub async fn set_tab_list_header_footer(&self, header: TextComponent, footer: TextComponent) { *self.tab_list_header.lock().await = header.clone(); *self.tab_list_footer.lock().await = footer.clone(); - self.client - .enqueue_packet(&CTabList::new(&header, &footer)) + self.send_client_packet(&CTabList::new(&header, &footer)) .await; } @@ -1084,8 +1085,7 @@ impl Player { duration, }, ); - self.client - .send_packet_now(&CItemCooldown::new(group, VarInt(duration))) + self.send_client_packet(&CItemCooldown::new(group, VarInt(duration))) .await; } @@ -2227,26 +2227,17 @@ impl Player { } pub async fn send_title_animation(&self, fade_in: i32, stay: i32, fade_out: i32) { - match self.client.as_ref() { - ClientPlatform::Java(client) => { - client - .enqueue_packet(&CTitleAnimation::new(fade_in, stay, fade_out)) - .await; - } - ClientPlatform::Bedrock(client) => { - client - .send_game_packet( - &pumpkin_protocol::bedrock::client::set_title::CSetTitle::new( - 5, - String::new(), - fade_in, - stay, - fade_out, - ), - ) - .await; - } - } + self.enqueue_packet_editioned( + &CTitleAnimation::new(fade_in, stay, fade_out), + &pumpkin_protocol::bedrock::client::set_title::CSetTitle::new( + 5, + String::new(), + fade_in, + stay, + fade_out, + ), + ) + .await; } pub fn spawn_particle( @@ -2257,7 +2248,7 @@ impl Player { particle_count: i32, particle: Particle, ) { - self.client.try_enqueue_packet(&CParticle::new( + let packet = CParticle::new( false, false, position, @@ -2266,7 +2257,12 @@ impl Player { particle_count, VarInt(particle as i32), &[], - )); + ); + if let ClientPlatform::Java(client) = self.client.as_ref() + && let Ok(data) = client.serialize_packet(&packet) + { + client.try_enqueue_packet(data); + } } pub async fn play_sound( @@ -2278,16 +2274,8 @@ impl Player { pitch: f32, seed: f64, ) { - self.client - .enqueue_packet(&CSoundEffect::new( - IdOr::Id(sound_id), - category, - position, - volume, - pitch, - seed, - )) - .await; + let packet = CSoundEffect::new(IdOr::Id(sound_id), category, position, volume, pitch, seed); + self.send_client_packet(&packet).await; } pub async fn play_sound_event( @@ -2299,16 +2287,8 @@ impl Player { pitch: f32, seed: f64, ) { - self.client - .enqueue_packet(&CSoundEffect::new( - IdOr::Value(sound), - category, - position, - volume, - pitch, - seed, - )) - .await; + let packet = CSoundEffect::new(IdOr::Value(sound), category, position, volume, pitch, seed); + self.send_client_packet(&packet).await; } /// Stops a sound playing on the client. @@ -2322,9 +2302,8 @@ impl Player { sound_id: Option, category: Option, ) { - self.client - .enqueue_packet(&CStopSound::new(sound_id, category)) - .await; + let packet = CStopSound::new(sound_id, category); + self.send_client_packet(&packet).await; } #[expect(clippy::too_many_lines)] @@ -2348,8 +2327,7 @@ impl Player { } else { // Target no longer exists, reset camera back to player self.camera_target_id.store(None); - self.client - .send_packet_now(&CSetCamera::new(self.entity_id().into())) + self.send_client_packet(&CSetCamera::new(self.entity_id().into())) .await; } } @@ -2416,9 +2394,9 @@ impl Player { && !self.bedrock_spawned.load(Ordering::Relaxed) && total_sent_chunks > 4 { - bedrock_client - .enqueue_packet(&CPlayStatus::PlayerSpawn) - .await; + if let Ok(data) = bedrock_client.serialize_packet(&CPlayStatus::PlayerSpawn) { + bedrock_client.enqueue_packet(data).await; + } self.bedrock_spawned.store(true, Ordering::Relaxed); self.set_client_loaded(true); self.send_health().await; @@ -2717,12 +2695,10 @@ impl Player { if abilities.creative { b |= 8; } - java.enqueue_packet(&CPlayerAbilities::new( - b, - abilities.fly_speed, - abilities.walk_speed, - )) - .await; + let packet = CPlayerAbilities::new(b, abilities.fly_speed, abilities.walk_speed); + if let Ok(data) = java.serialize_packet(&packet) { + java.enqueue_packet(data).await; + } } ClientPlatform::Bedrock(bedrock) => { let abilities = self.abilities.lock().await; @@ -2797,7 +2773,9 @@ impl Player { layers, }; - bedrock.send_game_packet(&packet).await; + if let Ok(data) = bedrock.serialize_packet(&packet) { + bedrock.send_game_packet(data).await; + } } } } @@ -2815,10 +2793,12 @@ impl Player { }) .collect(); - java.enqueue_packet(&CAwardStats { + let packet = CAwardStats { stats: &packet_stats, - }) - .await; + }; + if let Ok(data) = java.serialize_packet(&packet) { + java.enqueue_packet(data).await; + } } } @@ -3062,15 +3042,54 @@ impl Player { je_packet: &J, be_packet: &B, ) { - match self.client.as_ref() { - ClientPlatform::Java(client) => client.enqueue_packet(je_packet).await, - ClientPlatform::Bedrock(client) => client.enqueue_packet(be_packet).await, - } + self.client + .enqueue_packet_editioned(je_packet, be_packet) + .await; + } + + pub async fn enqueue_packet_editioned< + J: pumpkin_protocol::ClientPacket + Sync, + B: pumpkin_protocol::BClientPacket + Sync, + >( + &self, + je_packet: &J, + be_packet: &B, + ) { + self.client + .enqueue_packet_editioned(je_packet, be_packet) + .await; + } + + pub fn try_enqueue_packet_editioned< + J: pumpkin_protocol::ClientPacket + Sync, + B: pumpkin_protocol::BClientPacket + Sync, + >( + &self, + je_packet: &J, + be_packet: &B, + ) { + self.client + .try_enqueue_packet_editioned(je_packet, be_packet); + } + + pub async fn send_packet_now_editioned< + J: pumpkin_protocol::ClientPacket + Sync, + B: pumpkin_protocol::BClientPacket + Sync, + >( + &self, + je_packet: &J, + be_packet: &B, + ) { + self.client + .send_packet_now_editioned(je_packet, be_packet) + .await; } pub async fn send_client_packet(&self, packet: &C) { - if let ClientPlatform::Java(client) = self.client.as_ref() { - client.enqueue_packet(packet).await; + if let ClientPlatform::Java(client) = self.client.as_ref() + && let Ok(data) = client.serialize_packet(packet) + { + client.enqueue_packet(data).await; } } @@ -3129,8 +3148,7 @@ impl Player { pub async fn set_compass_target(&self, pos: pumpkin_util::math::position::BlockPos) { use pumpkin_protocol::java::client::play::CPlayerSpawnPosition; self.compass_target.store(Some(pos)); - self.client - .enqueue_packet(&CPlayerSpawnPosition::new(pos, 0.0, 0.0, String::new())) + self.send_client_packet(&CPlayerSpawnPosition::new(pos, 0.0, 0.0, String::new())) .await; } @@ -3272,8 +3290,7 @@ impl Player { level.clean_entity_chunks(&chunks_to_clean); } for chunk in &chunks_to_clean { - self.client - .enqueue_packet(&CUnloadChunk::new(chunk.x, chunk.y)) + self.send_client_packet(&CUnloadChunk::new(chunk.x, chunk.y)) .await; } @@ -3352,7 +3369,7 @@ impl Player { )); match self.client.as_ref() { ClientPlatform::Java(java) => { - java.send_packet_now(&CRespawn::new( + let packet = CRespawn::new( PlayerSpawnData::new( new_world.dimension.clone(), biome::hash_seed(new_world.level.seed.0), // seed @@ -3365,7 +3382,10 @@ impl Player { new_world.sea_level.into(), ), CRespawn::KEEP_ALL_DATA, - )).await; + ); + if let Ok(data) = java.serialize_packet(&packet) { + java.send_packet_now(data).await; + } } ClientPlatform::Bedrock(bedrock) => { let bedrock_dimension = if new_world.dimension == Dimension::OVERWORLD { @@ -3383,7 +3403,9 @@ impl Player { pos_f32, false, ); - bedrock.enqueue_packet(&change_dim_packet).await; + if let Ok(data) = bedrock.serialize_packet(&change_dim_packet) { + bedrock.enqueue_packet(data).await; + } self.bedrock_spawned.store(false, Ordering::Relaxed); } } @@ -3440,40 +3462,42 @@ impl Player { ClientPlatform::Java(client) => { *self.awaiting_teleport.lock().await = Some((teleport_id.into(), position)); - client - .send_packet_now(&CPlayerPosition::new( - teleport_id.into(), - position, - Vector3::new(0.0, 0.0, 0.0), - yaw, - pitch, - // TODO - Vec::new(), - )) - .await; + let packet = CPlayerPosition::new( + teleport_id.into(), + position, + Vector3::new(0.0, 0.0, 0.0), + yaw, + pitch, + // TODO + Vec::new(), + ); + if let Ok(data) = client.serialize_packet(&packet) { + client.send_packet_now(data).await; + } } ClientPlatform::Bedrock(client) => { - client - .send_game_packet(&CBedrockMovePlayer::new( - VarULong(self.entity_id() as u64), - Vector3::new( - position.x as f32, - position.y as f32 + entity.entity_type.eye_height, - position.z as f32, - ), - pitch, - yaw, - yaw, - CBedrockMovePlayer::MODE_TELEPORT, - false, - VarULong(0), - 0, - 0, - VarULong( - self.tick_counter.load(Ordering::Relaxed).max(0) as u64, - ), - )) - .await; + let packet = CBedrockMovePlayer::new( + VarULong(self.entity_id() as u64), + Vector3::new( + position.x as f32, + position.y as f32 + entity.entity_type.eye_height, + position.z as f32, + ), + pitch, + yaw, + yaw, + CBedrockMovePlayer::MODE_TELEPORT, + false, + VarULong(0), + 0, + 0, + VarULong( + self.tick_counter.load(Ordering::Relaxed).max(0) as u64, + ), + ); + if let Ok(data) = client.serialize_packet(&packet) { + client.send_game_packet(data).await; + } } } } @@ -3623,59 +3647,50 @@ impl Player { return; } - match self.client.as_ref() { - ClientPlatform::Java(client) => { - client - .enqueue_packet(&CSetHealth::new( + let max_health = self.living_entity.get_max_health(); + let attribute = |name: &str, current_value, max_value, default_value| BedrockAttribute { + min_value: 0.0, + max_value, + current_value, + default_min_value: 0.0, + default_max_value: max_value, + default_value, + name: name.to_string(), + modifiers_list_size: pumpkin_protocol::codec::var_uint::VarUInt(0), + }; + + self.enqueue_packet_editioned( + &CSetHealth::new( + self.living_entity.health.load(), + self.hunger_manager.level.load().into(), + self.hunger_manager.saturation.load(), + ), + &CBedrockAttributes { + runtime_id: VarULong(self.entity_id() as u64), + attributes: vec![ + attribute( + "minecraft:health", self.living_entity.health.load(), + max_health, + max_health, + ), + attribute( + "minecraft:player.hunger", self.hunger_manager.level.load().into(), + 20.0, + 20.0, + ), + attribute( + "minecraft:player.saturation", self.hunger_manager.saturation.load(), - )) - .await; - } - ClientPlatform::Bedrock(client) => { - let max_health = self.living_entity.get_max_health(); - let attribute = - |name: &str, current_value, max_value, default_value| BedrockAttribute { - min_value: 0.0, - max_value, - current_value, - default_min_value: 0.0, - default_max_value: max_value, - default_value, - name: name.to_string(), - modifiers_list_size: pumpkin_protocol::codec::var_uint::VarUInt(0), - }; - client - .enqueue_packet(&CBedrockAttributes { - runtime_id: VarULong(self.entity_id() as u64), - attributes: vec![ - attribute( - "minecraft:health", - self.living_entity.health.load(), - max_health, - max_health, - ), - attribute( - "minecraft:player.hunger", - self.hunger_manager.level.load().into(), - 20.0, - 20.0, - ), - attribute( - "minecraft:player.saturation", - self.hunger_manager.saturation.load(), - 20.0, - 5.0, - ), - ], - player_tick: VarULong( - self.tick_counter.load(Ordering::Relaxed).max(0) as u64 - ), - }) - .await; - } - } + 20.0, + 5.0, + ), + ], + player_tick: VarULong(self.tick_counter.load(Ordering::Relaxed).max(0) as u64), + }, + ) + .await; } async fn send_bedrock_respawn_state(&self, state: RespawnState) { @@ -3683,7 +3698,7 @@ impl Player { let entity = self.get_entity(); let position = entity.pos.load(); client - .send_game_packet(&CBedrockRespawn::new( + .send_packet(&CBedrockRespawn::new( Vector3::new( position.x as f32, position.y as f32 + entity.entity_type.eye_height, @@ -4034,7 +4049,7 @@ impl Player { if gamemode != GameMode::Spectator && self.camera_target_id.load().is_some() { self.camera_target_id.store(None); - self.client.send_packet_now(&CSetCamera::new( + self.send_client_packet(&CSetCamera::new( self.entity_id().into() )).await; } @@ -4165,14 +4180,13 @@ impl Player { sender_name: &TextComponent, target_name: Option<&TextComponent>, ) { - self.client - .enqueue_packet(&CDisguisedChatMessage::new( - message, - (chat_type + 1).into(), - sender_name, - target_name, - )) - .await; + self.send_client_packet(&CDisguisedChatMessage::new( + message, + (chat_type + 1).into(), + sender_name, + target_name, + )) + .await; } pub async fn drop_item(&self, item_stack: ItemStack) { @@ -4283,32 +4297,24 @@ impl Player { } pub async fn send_system_message_raw(&self, text: &TextComponent, overlay: bool) { - match self.client.as_ref() { - ClientPlatform::Java(client) => { - client - .enqueue_packet(&CSystemChatMessage::new(text, overlay)) - .await; + let je_packet = CSystemChatMessage::new(text, overlay); + let locale = Locale::from_str(&self.config.load().locale).unwrap_or(Locale::EnUs); + let be_packet = match &*text.0.content { + pumpkin_util::text::TextContent::Translate { + translate, + bedrock_translate, + with, + } => { + let key = bedrock_translate.as_deref().unwrap_or(translate.as_ref()); + let parameters = with + .iter() + .map(pumpkin_util::text::TextComponentBase::to_bedrock_string) + .collect(); + SText::translation(key.to_string(), parameters) } - ClientPlatform::Bedrock(client) => { - let locale = Locale::from_str(&self.config.load().locale).unwrap_or(Locale::EnUs); - let packet = match &*text.0.content { - pumpkin_util::text::TextContent::Translate { - translate, - bedrock_translate, - with, - } => { - let key = bedrock_translate.as_deref().unwrap_or(translate.as_ref()); - let parameters = with - .iter() - .map(pumpkin_util::text::TextComponentBase::to_bedrock_string) - .collect(); - SText::translation(key.to_string(), parameters) - } - _ => SText::system_message(text.0.to_bedrock_legacy(locale)), - }; - client.enqueue_packet(&packet).await; - } - } + _ => SText::system_message(text.0.to_bedrock_legacy(locale)), + }; + self.enqueue_packet_editioned(&je_packet, &be_packet).await; } pub async fn tick_experience(&self) { @@ -4323,13 +4329,12 @@ impl Player { self.last_sent_xp.store(level, Ordering::Relaxed); - self.client - .send_packet_now(&CSetExperience::new( - progress.clamp(0.0, 1.0), - level.into(), - points.into(), - )) - .await; + self.send_client_packet(&CSetExperience::new( + progress.clamp(0.0, 1.0), + level.into(), + points.into(), + )) + .await; } } @@ -4396,15 +4401,14 @@ impl Player { data: &*map_data.colors, }); - self.client - .enqueue_packet(&CMapItemData { - map_id: VarInt(map_id), - scale: map_data.scale, - locked: map_data.locked, - icons: Some(&icons), - data, - }) - .await; + self.send_client_packet(&CMapItemData { + map_id: VarInt(map_id), + scale: map_data.scale, + locked: map_data.locked, + icons: Some(&icons), + data, + }) + .await; map_data.dirty = false; } } @@ -4434,13 +4438,12 @@ impl Player { self.tick_experience().await; if self.has_client_loaded() { - self.client - .enqueue_packet(&CSetExperience::new( - progress.clamp(0.0, 1.0), - level.into(), - points.into(), - )) - .await; + self.send_client_packet(&CSetExperience::new( + progress.clamp(0.0, 1.0), + level.into(), + points.into(), + )) + .await; } } @@ -4510,27 +4513,25 @@ impl Player { } let effect_id = VarInt(i32::from(effect.effect_type.id)); - self.client - .enqueue_packet(&CUpdateMobEffect::new( - self.entity_id().into(), - effect_id, - effect.amplifier.into(), - effect.duration.into(), - flag, - )) - .await; + self.send_client_packet(&CUpdateMobEffect::new( + self.entity_id().into(), + effect_id, + effect.amplifier.into(), + effect.duration.into(), + flag, + )) + .await; } pub async fn remove_effect(&self, effect_type: &'static StatusEffect) -> bool { let effect_id = VarInt(i32::from(effect_type.id)); - self.client - .enqueue_packet( - &pumpkin_protocol::java::client::play::CRemoveMobEffect::new( - self.entity_id().into(), - effect_id, - ), - ) - .await; + self.send_client_packet( + &pumpkin_protocol::java::client::play::CRemoveMobEffect::new( + self.entity_id().into(), + effect_id, + ), + ) + .await; self.living_entity.remove_effect(effect_type).await @@ -4543,14 +4544,13 @@ impl Player { for effect in self.living_entity.active_effects.lock().await.keys() { effect_list.push(*effect); let effect_id = VarInt(i32::from(effect.id)); - self.client - .enqueue_packet( - &pumpkin_protocol::java::client::play::CRemoveMobEffect::new( - self.entity_id().into(), - effect_id, - ), - ) - .await; + self.send_client_packet( + &pumpkin_protocol::java::client::play::CRemoveMobEffect::new( + self.entity_id().into(), + effect_id, + ), + ) + .await; succeeded = true; } @@ -6342,7 +6342,9 @@ impl InventoryPlayer for Player { Box::pin(async move { match self.client.as_ref() { ClientPlatform::Java(java) => { - java.enqueue_packet(packet).await; + if let Ok(data) = java.serialize_packet(packet) { + java.enqueue_packet(data).await; + } } ClientPlatform::Bedrock(bedrock) => { use pumpkin_protocol::bedrock::{ @@ -6375,7 +6377,9 @@ impl InventoryPlayer for Player { }, storage_item: NetworkItemStackDescriptor::default(), }; - bedrock.enqueue_packet(&bedrock_packet).await; + if let Ok(data) = bedrock.serialize_packet(&bedrock_packet) { + bedrock.enqueue_packet(data).await; + } } else if matches!( window_type, Some( @@ -6408,7 +6412,9 @@ impl InventoryPlayer for Player { }, storage_item: NetworkItemStackDescriptor::default(), }; - bedrock.enqueue_packet(&bedrock_packet).await; + if let Ok(data) = bedrock.serialize_packet(&bedrock_packet) { + bedrock.enqueue_packet(data).await; + } } } } @@ -6424,7 +6430,9 @@ impl InventoryPlayer for Player { Box::pin(async move { match self.client.as_ref() { ClientPlatform::Java(java) => { - java.enqueue_packet(packet).await; + if let Ok(data) = java.serialize_packet(packet) { + java.enqueue_packet(data).await; + } } ClientPlatform::Bedrock(bedrock) => { use pumpkin_protocol::bedrock::{ @@ -6449,7 +6457,9 @@ impl InventoryPlayer for Player { storage: None, item: item_desc, }; - bedrock.enqueue_packet(&bedrock_packet).await; + if let Ok(data) = bedrock.serialize_packet(&bedrock_packet) { + bedrock.enqueue_packet(data).await; + } } } else { let slot_idx = packet.slot as usize; @@ -6493,7 +6503,9 @@ impl InventoryPlayer for Player { storage: None, item: item_desc, }; - bedrock.enqueue_packet(&bedrock_packet).await; + if let Ok(data) = bedrock.serialize_packet(&bedrock_packet) { + bedrock.enqueue_packet(data).await; + } } } } @@ -6505,7 +6517,9 @@ impl InventoryPlayer for Player { Box::pin(async move { match self.client.as_ref() { ClientPlatform::Java(java) => { - java.enqueue_packet(packet).await; + if let Ok(data) = java.serialize_packet(packet) { + java.enqueue_packet(data).await; + } } ClientPlatform::Bedrock(bedrock) => { use pumpkin_protocol::bedrock::{ @@ -6526,7 +6540,9 @@ impl InventoryPlayer for Player { }, storage_item: NetworkItemStackDescriptor::default(), }; - bedrock.enqueue_packet(&bedrock_packet).await; + if let Ok(data) = bedrock.serialize_packet(&bedrock_packet) { + bedrock.enqueue_packet(data).await; + } } } }) @@ -6537,7 +6553,7 @@ impl InventoryPlayer for Player { packet: &'a CSetContainerProperty, ) -> PlayerFuture<'a, ()> { Box::pin(async move { - self.client.enqueue_packet(packet).await; + self.send_client_packet(packet).await; }) } @@ -6548,7 +6564,9 @@ impl InventoryPlayer for Player { Box::pin(async move { match self.client.as_ref() { ClientPlatform::Java(java) => { - java.enqueue_packet(packet).await; + if let Ok(data) = java.serialize_packet(packet) { + java.enqueue_packet(data).await; + } } ClientPlatform::Bedrock(bedrock) => { use pumpkin_protocol::bedrock::{ @@ -6576,7 +6594,9 @@ impl InventoryPlayer for Player { storage: None, item: item_desc, }; - bedrock.enqueue_packet(&bedrock_packet).await; + if let Ok(data) = bedrock.serialize_packet(&bedrock_packet) { + bedrock.enqueue_packet(data).await; + } } } }) diff --git a/crates/pumpkin/src/entity/player/advancement.rs b/crates/pumpkin/src/entity/player/advancement.rs index 7694730ad..fe185e982 100644 --- a/crates/pumpkin/src/entity/player/advancement.rs +++ b/crates/pumpkin/src/entity/player/advancement.rs @@ -394,8 +394,7 @@ impl PlayerAdvancement { let first_packet = self.is_first_packet; tokio::spawn(async move { player - .client - .send_packet_now(&CUpdateAdvancements::new( + .send_client_packet(&CUpdateAdvancements::new( first_packet, added, parsed_progress, @@ -516,8 +515,7 @@ impl PlayerAdvancement { && let Some(player) = self.player.upgrade() { player - .client - .send_packet_now(&CSelectAdvancementsTab::new( + .send_client_packet(&CSelectAdvancementsTab::new( self.last_selected_tab.map(|adv| adv.id.clone()), )) .await; diff --git a/crates/pumpkin/src/item/items/writable_book.rs b/crates/pumpkin/src/item/items/writable_book.rs index 603a27e2e..00717dd5f 100644 --- a/crates/pumpkin/src/item/items/writable_book.rs +++ b/crates/pumpkin/src/item/items/writable_book.rs @@ -26,8 +26,7 @@ impl ItemBehaviour for WritableBookItem { Box::pin(async move { if item.id == Item::WRITTEN_BOOK.id { player - .client - .enqueue_packet(&COpenBook::new(VarInt(0))) // 0 = main hand + .send_client_packet(&COpenBook::new(VarInt(0))) // 0 = main hand .await; player.world().play_sound( Sound::ItemBookPageTurn, diff --git a/crates/pumpkin/src/lib.rs b/crates/pumpkin/src/lib.rs index 8df557a97..a90479804 100644 --- a/crates/pumpkin/src/lib.rs +++ b/crates/pumpkin/src/lib.rs @@ -18,7 +18,7 @@ use crate::net::bedrock::{ }; use crate::net::java::JavaClient; use crate::net::java::pending::PendingConnection; -use crate::net::{ClientPlatform, DisconnectReason, PacketHandlerResult}; +use crate::net::{ClientPlatform, DisconnectReason, PacketHandlerResult, PacketRateLimiter}; use crate::net::{lan_broadcast::LANBroadcast, query, rcon::RCONServer}; use crate::plugin::server::server_command::ServerCommandEvent; use crate::server::{Server, ticker::Ticker}; @@ -542,7 +542,15 @@ impl PumpkinServer { let server_clone = self.server.clone(); tasks.spawn(async move { - let mut pending = PendingConnection::new(connection, client_addr, client_id); + let packet_limiter = PacketRateLimiter::from_config( + &server_clone.advanced_config.networking.java.packet_limiter, + ); + let mut pending = PendingConnection::new( + connection, + client_addr, + client_id, + packet_limiter, + ); let login_result = pending.handle_login_sequence(&server_clone).await; match login_result { @@ -609,10 +617,14 @@ impl PumpkinServer { if let Some((session, client_addr)) = nethernet_result { *master_client_id_counter += 1; let be_clients = bedrock_clients.clone(); + let packet_limiter = PacketRateLimiter::from_config( + &self.server.advanced_config.networking.bedrock.packet_limiter, + ); let client = Arc::new(BedrockClient::new( session.clone(), client_addr, be_clients, + packet_limiter, )); client.start_outgoing_packet_task(); bedrock_clients.lock().await.insert(client_addr, client.clone()); diff --git a/crates/pumpkin/src/net/bedrock/login/login.rs b/crates/pumpkin/src/net/bedrock/login/login.rs index f21d187a4..f0dd32372 100644 --- a/crates/pumpkin/src/net/bedrock/login/login.rs +++ b/crates/pumpkin/src/net/bedrock/login/login.rs @@ -80,8 +80,7 @@ impl BedrockClient { )); } - self.enqueue_packet_internal(&CPlayStatus::LoginSuccess) - .await; + self.enqueue_client_packet(&CPlayStatus::LoginSuccess).await; let br_config = &server.advanced_config.resource_pack.bedrock; let mut entries = Vec::new(); @@ -111,7 +110,7 @@ impl BedrockClient { world_template_version: String::new(), resource_packs: entries, }; - self.enqueue_packet_internal(&packs_info).await; + self.enqueue_client_packet(&packs_info).await; let new_config = PlayerConfig { locale: client_data.language_code.clone(), diff --git a/crates/pumpkin/src/net/bedrock/login/request_network_settings.rs b/crates/pumpkin/src/net/bedrock/login/request_network_settings.rs index 42aacda77..76fcd45e2 100644 --- a/crates/pumpkin/src/net/bedrock/login/request_network_settings.rs +++ b/crates/pumpkin/src/net/bedrock/login/request_network_settings.rs @@ -8,10 +8,10 @@ impl BedrockClient { server: &Server, ) { if packet.protocol_version < CURRENT_BEDROCK_MC_PROTOCOL as i32 { - self.send_game_packet(&CPlayStatus::OutdatedClient).await; + self.send_packet(&CPlayStatus::OutdatedClient).await; return; } else if packet.protocol_version > CURRENT_BEDROCK_MC_PROTOCOL as i32 { - self.send_game_packet(&CPlayStatus::OutdatedServer).await; + self.send_packet(&CPlayStatus::OutdatedServer).await; return; } @@ -26,7 +26,7 @@ impl BedrockClient { .compression .info .clone(); - self.send_game_packet(&CNetworkSettings::new( + self.send_packet(&CNetworkSettings::new( compression.threshold as u16, 0, false, diff --git a/crates/pumpkin/src/net/bedrock/login/resource_pack_response.rs b/crates/pumpkin/src/net/bedrock/login/resource_pack_response.rs index 196fa7d21..81b37332c 100644 --- a/crates/pumpkin/src/net/bedrock/login/resource_pack_response.rs +++ b/crates/pumpkin/src/net/bedrock/login/resource_pack_response.rs @@ -40,7 +40,7 @@ impl BedrockClient { Vec::new() }; - self.enqueue_packet_internal(&CResourcePackStackPacket::new( + self.enqueue_client_packet(&CResourcePackStackPacket::new( br_config.force, resource_packs, CURRENT_BEDROCK_MC_VERSION.to_string(), diff --git a/crates/pumpkin/src/net/bedrock/mod.rs b/crates/pumpkin/src/net/bedrock/mod.rs index 2c5a449b5..286da7186 100644 --- a/crates/pumpkin/src/net/bedrock/mod.rs +++ b/crates/pumpkin/src/net/bedrock/mod.rs @@ -58,7 +58,7 @@ pub mod login; use self::nethernet::NetherNetSession; use crate::{ entity::player::Player, - net::{DisconnectReason, PacketHandlerResult}, + net::{DisconnectReason, PacketHandlerResult, PacketRateLimiter}, plugin::api::events::world::chunk_send::ChunkSend, server::Server, }; @@ -123,6 +123,8 @@ pub struct BedrockClient { last_seen: Arc>, incoming_game_packet_send: Sender, incoming_game_packet_recv: Mutex>>, + /// Packet rate limiter for incoming client packets. + pub packet_limiter: PacketRateLimiter, } impl BedrockClient { @@ -131,6 +133,7 @@ impl BedrockClient { session: Arc, address: SocketAddr, be_clients: Arc>>>, + packet_limiter: PacketRateLimiter, ) -> Self { let (send, recv) = tokio::sync::mpsc::channel(4096); let (priority_send, priority_recv) = tokio::sync::mpsc::channel(4096); @@ -159,6 +162,7 @@ impl BedrockClient { last_seen: Arc::new(AtomicCell::new(std::time::Instant::now())), incoming_game_packet_send: incoming_send, incoming_game_packet_recv: Mutex::new(Some(incoming_recv)), + packet_limiter, } } @@ -266,7 +270,7 @@ impl BedrockClient { } pub async fn kick(&self, reason: DisconnectReason, message: String) { - self.send_game_packet(&CDisconnectPlayer::new(reason as i32, message)) + self.send_packet(&CDisconnectPlayer::new(reason as i32, message)) .await; self.close().await; } @@ -280,7 +284,7 @@ impl BedrockClient { send_packet: bool, ) { if send_packet { - self.send_game_packet(&CDisconnectPlayer { + self.send_packet(&CDisconnectPlayer { reason: pumpkin_protocol::codec::var_int::VarInt(reason as i32), skip_message, message, @@ -383,62 +387,12 @@ impl BedrockClient { self.player.store(Arc::new(Some(player))); } - pub async fn enqueue_packet(&self, packet: &P) { - let mut packet_buf = Vec::new(); - match self.write_game_packet(packet, &mut packet_buf).await { - Ok(()) => { - let payload = Bytes::from(packet_buf); - let player = self.player.load_full(); - if let Some(player) = player.as_ref() { - let event = player - .fire_packet_sent_event_no_obj(P::PACKET_ID, payload) - .await; - if !event.cancelled { - self.enqueue_packet_data(event.payload).await; - } - } else { - self.enqueue_packet_data(payload).await; - } - } - Err(err) => error!("Failed to write game packet: {err}"), - } + pub async fn enqueue_packet(&self, packet_data: Bytes) { + self.enqueue_packet_data(packet_data).await; } - pub async fn enqueue_packet_internal(&self, packet: &P) { - let mut packet_buf = Vec::new(); - match self.write_game_packet(packet, &mut packet_buf).await { - Ok(()) => self.enqueue_packet_data(packet_buf.into()).await, - Err(err) => error!("Failed to write game packet: {err}"), - } - } - - pub fn try_enqueue_packet(&self, packet: &P) { - let mut packet_buf = Vec::new(); - let mut packet_payload = Vec::new(); - if let Err(err) = packet.write_packet(&mut packet_payload) { - error!("Failed to write packet for try_enqueue_packet: {err}"); - return; - } - - { - let Ok(network_writer) = self.network_writer.try_read() 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()); + pub fn try_enqueue_packet(&self, packet_data: Bytes) { + self.try_enqueue_packet_data(packet_data); } /// Queues a clientbound packet to be sent to the connected client. Queued chunks are sent @@ -446,7 +400,7 @@ impl BedrockClient { /// /// # Arguments /// - /// * `packet`: A reference to a packet object implementing the `ClientPacket` trait. + /// * `packet_data`: A `Bytes` payload representing the encoded packet. pub async fn enqueue_packet_data(&self, packet_data: Bytes) { if let Err(err) = self .outgoing_packet_queue_send @@ -508,37 +462,38 @@ impl BedrockClient { ) } - pub async fn send_game_packet(&self, packet: &P) { - let mut packet_buf = Vec::new(); - match self.write_game_packet(packet, &mut packet_buf).await { - Ok(()) => { - let payload = Bytes::from(packet_buf); - let player = self.player.load_full(); - let payload = if let Some(player) = player.as_ref() { - let event = player - .fire_packet_sent_event_no_obj(P::PACKET_ID, payload) - .await; - if event.cancelled { - return; - } - event.payload - } else { - payload - }; - let (tx, rx) = oneshot::channel(); - if let Err(err) = self - .outgoing_packet_priority_send - .send(OutgoingPacket::priority(payload, tx)) - .await - { - if !self.is_closed() { - error!("Failed to add priority packet to the outgoing packet queue: {err}"); - } - } else { - let _ = rx.await; - } + pub fn serialize_packet(&self, packet: &P) -> Result { + let encoder = self.network_writer.try_read(); + encoder.map_or_else( + |_| pumpkin_protocol::bedrock::packet_encoder::serialize_packet(packet), + |encoder| encoder.serialize_packet(packet), + ) + } + + pub async fn send_packet(&self, packet: &P) { + if let Ok(data) = self.serialize_packet(packet) { + self.send_game_packet(data).await; + } + } + + pub async fn enqueue_client_packet(&self, packet: &P) { + if let Ok(data) = self.serialize_packet(packet) { + self.enqueue_packet(data).await; + } + } + + pub async fn send_game_packet(&self, packet_data: Bytes) { + let (tx, rx) = oneshot::channel(); + if let Err(err) = self + .outgoing_packet_priority_send + .send(OutgoingPacket::priority(packet_data, tx)) + .await + { + if !self.is_closed() { + error!("Failed to add priority packet to the outgoing packet queue: {err}"); } - Err(err) => error!("Failed to write game packet: {err}"), + } else { + let _ = rx.await; } } @@ -585,6 +540,27 @@ impl BedrockClient { .await .get_game_packet(&mut cursor) .map_err(|e| Error::other(e.to_string()))?; + + if !self.packet_limiter.check_packet() { + warn!( + "Bedrock client {} exceeded packet rate limit (rate: {}/s)", + self.address, + self.packet_limiter.max_rate() + ); + self.kick( + DisconnectReason::Kicked, + server + .advanced_config + .networking + .bedrock + .packet_limiter + .kick_message + .clone(), + ) + .await; + return Err(Error::other("Packet rate limit exceeded")); + } + self.handle_game_packet(server, game_packet).await?; } @@ -811,7 +787,7 @@ impl BedrockClient { } } if !missing_blobs.is_empty() { - self.send_game_packet(&CClientCacheMissResponse { + self.send_packet(&CClientCacheMissResponse { blobs: &missing_blobs, }) .await; diff --git a/crates/pumpkin/src/net/bedrock/play/block_pick_request.rs b/crates/pumpkin/src/net/bedrock/play/block_pick_request.rs index ef9dca8dc..df816a249 100644 --- a/crates/pumpkin/src/net/bedrock/play/block_pick_request.rs +++ b/crates/pumpkin/src/net/bedrock/play/block_pick_request.rs @@ -90,7 +90,7 @@ impl BedrockClient { player.living_entity.send_equipment_changes(equipment); // Sync bedrock inventory updates - self.enqueue_packet(&CInventoryContent { + self.enqueue_client_packet(&CInventoryContent { container_id: VarUInt(0), slots: player .inventory() diff --git a/crates/pumpkin/src/net/bedrock/play/container_close.rs b/crates/pumpkin/src/net/bedrock/play/container_close.rs index dece7395c..24c27b2c0 100644 --- a/crates/pumpkin/src/net/bedrock/play/container_close.rs +++ b/crates/pumpkin/src/net/bedrock/play/container_close.rs @@ -8,7 +8,7 @@ impl BedrockClient { } player.on_handled_screen_closed().await; - self.enqueue_packet(&SContainerClose { + self.enqueue_client_packet(&SContainerClose { container_id: packet.container_id, container_type: packet.container_type, server_initiated: false, @@ -16,7 +16,7 @@ impl BedrockClient { .await; // Sync the cursor (make it empty) to Bedrock client - self.enqueue_packet(&CInventoryContent { + self.enqueue_client_packet(&CInventoryContent { container_id: VarUInt(59), // Cursor container ID slots: vec![NetworkItemStackDescriptor::default()], full_container_name: FullContainerName { @@ -28,7 +28,7 @@ impl BedrockClient { .await; // Sync the inventory content to Bedrock client - self.enqueue_packet(&CInventoryContent { + self.enqueue_client_packet(&CInventoryContent { container_id: VarUInt(0), // player inventory slots: player .inventory() diff --git a/crates/pumpkin/src/net/bedrock/play/interaction.rs b/crates/pumpkin/src/net/bedrock/play/interaction.rs index e58adace9..c3f3b4a9c 100644 --- a/crates/pumpkin/src/net/bedrock/play/interaction.rs +++ b/crates/pumpkin/src/net/bedrock/play/interaction.rs @@ -30,7 +30,7 @@ impl BedrockClient { return; } self.inventory_opened.store(true, Ordering::Relaxed); - self.enqueue_packet(&CContainerOpen { + self.enqueue_client_packet(&CContainerOpen { container_id: 0, container_type: 0xff, position: BlockPos::ZERO, diff --git a/crates/pumpkin/src/net/bedrock/play/inventory_action.rs b/crates/pumpkin/src/net/bedrock/play/inventory_action.rs index b84befdeb..f4f27fd00 100644 --- a/crates/pumpkin/src/net/bedrock/play/inventory_action.rs +++ b/crates/pumpkin/src/net/bedrock/play/inventory_action.rs @@ -119,7 +119,7 @@ impl BedrockClient { } if inventory_updated { - self.enqueue_packet(&CInventoryContent { + self.enqueue_client_packet(&CInventoryContent { container_id: VarUInt(0), slots: player .inventory() @@ -444,7 +444,7 @@ impl BedrockClient { } } - self.enqueue_packet(&CItemStackResponse { + self.enqueue_client_packet(&CItemStackResponse { responses: vec![ItemStackResponse { result, request_id: packet.legacy_request_id, diff --git a/crates/pumpkin/src/net/bedrock/play/item_stack_request.rs b/crates/pumpkin/src/net/bedrock/play/item_stack_request.rs index 8c2f25eac..cf80713e1 100644 --- a/crates/pumpkin/src/net/bedrock/play/item_stack_request.rs +++ b/crates/pumpkin/src/net/bedrock/play/item_stack_request.rs @@ -514,10 +514,11 @@ impl BedrockClient { } // Send Bedrock specific responses and updates - self.enqueue_packet(&CItemStackResponse { responses }).await; + self.enqueue_client_packet(&CItemStackResponse { responses }) + .await; if inventory_updated { - self.enqueue_packet(&CInventoryContent { + self.enqueue_client_packet(&CInventoryContent { container_id: VarUInt(0), slots: player .inventory() diff --git a/crates/pumpkin/src/net/bedrock/play/mob_equipment.rs b/crates/pumpkin/src/net/bedrock/play/mob_equipment.rs index 823dbf253..4f856f8be 100644 --- a/crates/pumpkin/src/net/bedrock/play/mob_equipment.rs +++ b/crates/pumpkin/src/net/bedrock/play/mob_equipment.rs @@ -23,7 +23,7 @@ impl BedrockClient { event.cancelled() }; if is_cancelled { - self.enqueue_packet(&CPlayerHotbar { + self.enqueue_client_packet(&CPlayerHotbar { selected_slot: VarUInt(previous_slot as u32), container_id: 0, should_select_block: true, diff --git a/crates/pumpkin/src/net/bedrock/play/player_action.rs b/crates/pumpkin/src/net/bedrock/play/player_action.rs index 06e3edc1e..83af61e08 100644 --- a/crates/pumpkin/src/net/bedrock/play/player_action.rs +++ b/crates/pumpkin/src/net/bedrock/play/player_action.rs @@ -167,7 +167,7 @@ impl BedrockClient { } } else { let runtime_id = pumpkin_data::BlockState::to_be_network_id(state.id); - self.enqueue_packet(&CUpdateBlock::new(location, runtime_id as u32)) + self.enqueue_client_packet(&CUpdateBlock::new(location, runtime_id as u32)) .await; if matches!(action, PlayerAction::StopBreak) { player.stop_mining().await; diff --git a/crates/pumpkin/src/net/bedrock/play/request_chunk_radius.rs b/crates/pumpkin/src/net/bedrock/play/request_chunk_radius.rs index 584941c5a..d9e063b28 100644 --- a/crates/pumpkin/src/net/bedrock/play/request_chunk_radius.rs +++ b/crates/pumpkin/src/net/bedrock/play/request_chunk_radius.rs @@ -25,7 +25,7 @@ impl BedrockClient { NonZero::::from(server.advanced_config.networking.bedrock.view_distance).get(), ); - self.enqueue_packet(&CChunkRadiusUpdate { + self.enqueue_client_packet(&CChunkRadiusUpdate { chunk_radius: VarInt(view_distance), }) .await; diff --git a/crates/pumpkin/src/net/bedrock/play/respawn.rs b/crates/pumpkin/src/net/bedrock/play/respawn.rs index 70c20b577..7fb7baed4 100644 --- a/crates/pumpkin/src/net/bedrock/play/respawn.rs +++ b/crates/pumpkin/src/net/bedrock/play/respawn.rs @@ -12,7 +12,7 @@ impl BedrockClient { let entity = player.get_entity(); let position = entity.pos.load(); - self.enqueue_packet(&CRespawn::new( + self.enqueue_client_packet(&CRespawn::new( pumpkin_util::math::vector3::Vector3::new( position.x as f32, position.y as f32 + entity.entity_type.eye_height, diff --git a/crates/pumpkin/src/net/java/config/keep_alive.rs b/crates/pumpkin/src/net/java/config/keep_alive.rs index a471e42c7..196d4d996 100644 --- a/crates/pumpkin/src/net/java/config/keep_alive.rs +++ b/crates/pumpkin/src/net/java/config/keep_alive.rs @@ -2,17 +2,24 @@ use super::*; impl JavaClient { - pub async fn handle_config_keep_alive(&self, keep_alive: SKeepAlive) { - if self.wait_for_keep_alive.load(Ordering::Relaxed) - && keep_alive.keep_alive_id == self.keep_alive_id.load() + pub fn handle_config_keep_alive(&self, keep_alive: &SKeepAlive) { + let mut pending = self + .pending_keep_alives + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + if let Some(pos) = pending + .iter() + .position(|(id, _)| *id == keep_alive.keep_alive_id) { + pending.swap_remove(pos); + self.wait_for_keep_alive.store(false, Ordering::Relaxed); + } else if keep_alive.keep_alive_id == self.keep_alive_id.load() { self.wait_for_keep_alive.store(false, Ordering::Relaxed); } else { - self.kick(pumpkin_macros::translate_cross!( - translation::java::DISCONNECT_TIMEOUT, - translation::bedrock::DISCONNECT_TIMEOUT - )) - .await; + debug!( + "Ignored unexpected config keep alive id {}", + keep_alive.keep_alive_id + ); } } } diff --git a/crates/pumpkin/src/net/java/config/known_packs.rs b/crates/pumpkin/src/net/java/config/known_packs.rs index d8441c696..8f551c250 100644 --- a/crates/pumpkin/src/net/java/config/known_packs.rs +++ b/crates/pumpkin/src/net/java/config/known_packs.rs @@ -11,7 +11,7 @@ impl JavaClient { // let mut tags_to_send = Vec::new(); let version = self.version.load(); if version >= JavaMinecraftVersion::V_1_20_2 { - self.send_packet_now(&CFeatureFlags::new(&["minecraft:vanilla".to_string()])) + self.send_packet(&CFeatureFlags::new(&["minecraft:vanilla".to_string()])) .await; let registry = Registry::get_synced(version); let mut sent_dimension_type = false; @@ -19,7 +19,7 @@ impl JavaClient { if reg.registry_id == "minecraft:dimension_type" { sent_dimension_type = true; } - self.send_packet_now(&CRegistryData::new(®.registry_id, ®.registry_entries)) + self.send_packet(&CRegistryData::new(®.registry_id, ®.registry_entries)) .await; } if !sent_dimension_type { @@ -36,7 +36,7 @@ impl JavaClient { data: Some(build_dimension_nbt(dim).into_boxed_slice()), }) .collect(); - self.send_packet_now(&CRegistryData::new( + self.send_packet(&CRegistryData::new( &"minecraft:dimension_type".to_string(), &dim_entries, )) @@ -71,10 +71,10 @@ impl JavaClient { tags.push(key); } } - self.send_packet_now(&CUpdateTags::new(&tags)).await; + self.send_packet(&CUpdateTags::new(&tags)).await; // We are done with configuring - self.send_packet_now(&CFinishConfig).await; + self.send_packet(&CFinishConfig).await; if version < JavaMinecraftVersion::V_1_20_2 { return Some(self.handle_config_acknowledged(server).await); diff --git a/crates/pumpkin/src/net/java/config/mod.rs b/crates/pumpkin/src/net/java/config/mod.rs index aab4ad000..2a6bb6f6e 100644 --- a/crates/pumpkin/src/net/java/config/mod.rs +++ b/crates/pumpkin/src/net/java/config/mod.rs @@ -9,7 +9,7 @@ use crate::{ server::Server, }; use core::str; -use pumpkin_data::{registry::Registry, translation}; +use pumpkin_data::registry::Registry; use pumpkin_protocol::{ ConnectionState, KnownPack, java::{ diff --git a/crates/pumpkin/src/net/java/config/resource_pack.rs b/crates/pumpkin/src/net/java/config/resource_pack.rs index dcc2308b4..da174d573 100644 --- a/crates/pumpkin/src/net/java/config/resource_pack.rs +++ b/crates/pumpkin/src/net/java/config/resource_pack.rs @@ -74,7 +74,7 @@ impl JavaClient { pub async fn send_known_packs(&self) { let version_str = self.version.load().to_string(); - self.send_packet_now(&CKnownPacks::new(&[KnownPack { + self.send_packet(&CKnownPacks::new(&[KnownPack { namespace: "minecraft", id: "core", version: &version_str, diff --git a/crates/pumpkin/src/net/java/mod.rs b/crates/pumpkin/src/net/java/mod.rs index 2056b42b8..c24f87032 100644 --- a/crates/pumpkin/src/net/java/mod.rs +++ b/crates/pumpkin/src/net/java/mod.rs @@ -63,7 +63,7 @@ use arc_swap::ArcSwap; use pending::PendingConnection; use crate::entity::player::Player; -use crate::net::{GameProfile, PacketHandlerResult, PlayerConfig}; +use crate::net::{GameProfile, PacketHandlerResult, PacketRateLimiter, PlayerConfig}; use crate::plugin::api::events::world::chunk_send::ChunkSend; use crate::plugin::player::player_custom_payload::PlayerCustomPayloadEvent; use crate::{error::PumpkinError, server::Server}; @@ -109,8 +109,14 @@ pub struct JavaClient { pub keep_alive_id: AtomicCell, /// The last time we sent a keep alive packet. pub last_keep_alive_time: AtomicCell, + /// The last time any packet was received from the client. + pub last_packet_time: AtomicCell, + /// Recent in-flight keep alive IDs with their sent timestamps. + pub pending_keep_alives: std::sync::Mutex>, pub packet_sequence: AtomicI32, + /// Packet rate limiter for incoming client packets. + pub packet_limiter: PacketRateLimiter, } pub enum OutgoingPacketType { @@ -170,7 +176,10 @@ impl JavaClient { wait_for_keep_alive: AtomicBool::new(false), keep_alive_id: AtomicCell::new(0), last_keep_alive_time: AtomicCell::new(Instant::now()), + last_packet_time: AtomicCell::new(Instant::now()), + pending_keep_alives: std::sync::Mutex::new(Vec::new()), packet_sequence: AtomicI32::new(-1), + packet_limiter: pending.packet_limiter, } } @@ -178,6 +187,7 @@ impl JavaClient { self.player.store(Arc::new(Some(player))); } + #[expect(clippy::too_many_lines)] pub async fn progress_player_packets(&self, player: &Arc, server: &Arc) { let Some(mut network_reader) = self .network_reader @@ -188,7 +198,11 @@ impl JavaClient { return; }; - let mut keep_alive_interval = tokio::time::interval(std::time::Duration::from_secs(15)); + let keep_alive_time = server.advanced_config.networking.java.keep_alive_time; + let mut keep_alive_interval = + tokio::time::interval(std::time::Duration::from_secs(keep_alive_time.max(1))); + let timeout_duration = + std::time::Duration::from_secs(keep_alive_time.saturating_mul(2).max(1)); // Skip the immediate first tick so we don't send a keep-alive the exact millisecond they join keep_alive_interval.tick().await; @@ -197,8 +211,17 @@ impl JavaClient { tokio::select! { // KEEP-ALIVE TIMER _ = keep_alive_interval.tick() => { - // If the client never responded to the LAST keep-alive, they timed out. - if self.wait_for_keep_alive.load(Ordering::Relaxed) { + // Check if the client has timed out on keep-alive responses or no packet activity + let has_timed_out = { + let pending = self + .pending_keep_alives + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + pending.iter().any(|(_, send_time)| send_time.elapsed() > timeout_duration) + } || (self.wait_for_keep_alive.load(Ordering::Relaxed) && self.last_keep_alive_time.load().elapsed() > timeout_duration) + || (self.last_packet_time.load().elapsed() > timeout_duration); + + if has_timed_out { self.kick(pumpkin_macros::translate_cross!(translation::java::DISCONNECT_TIMEOUT, translation::bedrock::DISCONNECT_TIMEOUT)).await; break; } @@ -212,8 +235,18 @@ impl JavaClient { self.keep_alive_id.store(keep_alive_id); self.wait_for_keep_alive.store(true, Ordering::Relaxed); self.last_keep_alive_time.store(Instant::now()); + { + let mut pending = self + .pending_keep_alives + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + pending.push((keep_alive_id, Instant::now())); + if pending.len() > 16 { + pending.remove(0); + } + } let packet = pumpkin_protocol::java::client::play::CKeepAlive::new(keep_alive_id); - self.enqueue_packet(&packet).await; + self.enqueue_client_packet(&packet).await; } // INCOMING PACKETS @@ -221,6 +254,27 @@ impl JavaClient { let Some(packet) = packet_opt else { break; }; + self.last_packet_time.store(Instant::now()); + + if !self.packet_limiter.check_packet() { + warn!( + "Client {} ({}) exceeded packet rate limit (rate: {}/s)", + self.id, + self.gameprofile.name, + self.packet_limiter.max_rate() + ); + self.kick(TextComponent::text( + server + .advanced_config + .networking + .java + .packet_limiter + .kick_message + .clone(), + )) + .await; + break; + } match self.handle_play_packet(player, server, &packet).await { Ok(()) => {} @@ -249,7 +303,7 @@ impl JavaClient { // it interacted with and drops our updates for that position let seq = self.packet_sequence.swap(-1, Ordering::Relaxed); if seq != -1 { - self.send_packet_now(&CAcknowledgeBlockChange::new(seq.into())) + self.send_packet(&CAcknowledgeBlockChange::new(seq.into())) .await; } } @@ -282,10 +336,6 @@ impl JavaClient { pub async fn send_chunks(&self, chunks: &[SyncChunk]) { let player = self.player.load_full(); let Some(player) = player.as_ref() else { - debug!( - "send_chunks: player not set yet, dropping {} chunks", - chunks.len() - ); return; }; let Some(server) = player.world().server.upgrade() else { @@ -293,7 +343,7 @@ impl JavaClient { }; if self.version.load() >= JavaMinecraftVersion::V_1_20_2 { - self.send_packet_now(&CChunkBatchStart).await; + self.send_packet(&CChunkBatchStart).await; } for chunk in chunks { let mut event = ChunkSend::new(player.world(), chunk.clone()); @@ -315,55 +365,15 @@ impl JavaClient { self.send_packet_now_data(buf.into()).await; } if self.version.load() >= JavaMinecraftVersion::V_1_20_2 { - self.send_packet_now(&CChunkBatchEnd::new(chunks.len() as u16)) + self.send_packet(&CChunkBatchEnd::new(chunks.len() as u16)) .await; } } - pub async fn enqueue_packet(&self, packet: &P) { - let mut buf = Vec::new(); - let writer = &mut buf; - if let Err(err) = self.write_packet(packet, writer) { - error!("Failed to write packet: {err:?}"); - return; - } - if buf.is_empty() { - return; - } - let payload = Bytes::from(buf); - - let player = self.player.load_full(); - if let Some(player) = player.as_ref() { - let event = player - .fire_packet_sent_event_no_obj(P::to_id(self.version.load()), payload) - .await; - if !event.cancelled { - self.enqueue_packet_data(event.payload).await; - } - } else { - self.enqueue_packet_data(payload).await; - } + pub async fn enqueue_packet(&self, packet_data: Bytes) { + self.enqueue_packet_data(packet_data).await; } - pub fn try_enqueue_packet(&self, packet: &P) { - let mut buf = Vec::new(); - let writer = &mut buf; - if let Err(err) = self.write_packet(packet, writer) { - error!("Failed to write packet: {err:?}"); - return; - } - if buf.is_empty() { - return; - } - self.try_enqueue_packet_data(buf.into()); - } - - /// Queues a clientbound packet to be sent to the connected client. Queued chunks are sent - /// in-order to the client - /// - /// # Arguments - /// - /// * `packet`: A reference to a packet object implementing the `ClientPacket` trait. pub async fn enqueue_packet_data(&self, packet_data: Bytes) { if let Err(err) = self .outgoing_packet_queue_send @@ -372,14 +382,21 @@ impl JavaClient { { // This is expected to fail if we are closed if !self.close_token.is_cancelled() { - error!( + warn!( "Failed to add packet to the outgoing packet queue for client {}: {}", self.id, err ); + // We now need to close the connection to the client since the stream is in an + // unknown state + self.close(); } } } + pub fn try_enqueue_packet(&self, packet_data: Bytes) { + self.try_enqueue_packet_data(packet_data); + } + pub fn try_enqueue_packet_data(&self, packet_data: Bytes) { if let Err(err) = self .outgoing_packet_queue_send @@ -394,10 +411,11 @@ impl JavaClient { } tokio::sync::mpsc::error::TrySendError::Closed(_) => { if !self.close_token.is_cancelled() { - error!( + warn!( "Failed to add packet to the outgoing packet queue for client {}: channel closed", self.id ); + self.close(); } } } @@ -442,16 +460,16 @@ impl JavaClient { match self.connection_state.load() { ConnectionState::Login => { // TextComponent implements Serialize and writes in bytes instead of String, that's the reason we only use content - self.send_packet_now(&CLoginDisconnect::new( + self.send_packet(&CLoginDisconnect::new( serde_json::to_string(&reason.0).unwrap_or_else(|_| String::new()), )) .await; } ConnectionState::Config => { - self.send_packet_now(&CConfigDisconnect::new(&reason.clone().get_text())) + self.send_packet(&CConfigDisconnect::new(&reason.clone().get_text())) .await; } - ConnectionState::Play => self.send_packet_now(&CPlayDisconnect::new(reason)).await, + ConnectionState::Play => self.send_packet(&CPlayDisconnect::new(reason)).await, _ => {} } } @@ -459,29 +477,8 @@ impl JavaClient { self.close(); } - pub async fn send_packet_now(&self, packet: &P) { - let mut packet_buf = Vec::new(); - let writer = &mut packet_buf; - if let Err(err) = self.write_packet(packet, writer) { - error!("Failed to write packet: {err:?}"); - return; - } - if packet_buf.is_empty() { - return; - } - let payload = Bytes::from(packet_buf); - - let player = self.player.load_full(); - if let Some(player) = player.as_ref() { - let event = player - .fire_packet_sent_event_no_obj(P::to_id(self.version.load()), payload) - .await; - if !event.cancelled { - self.send_packet_now_data(event.payload).await; - } - } else { - self.send_packet_now_data(payload).await; - } + pub async fn send_packet_now(&self, packet: Bytes) { + self.send_packet_now_data(packet).await; } pub async fn send_packet_now_data(&self, packet: Bytes) { @@ -514,24 +511,32 @@ impl JavaClient { pub fn write_packet_for_version( packet: &P, version: JavaMinecraftVersion, - mut write: impl Write, + write: impl Write, ) -> Result<(), WritingError> { - let version_number = P::to_id(version); - if version_number == -1 { - return Ok(()); - } - write.write_var_int(&VarInt(version_number))?; - packet.write_packet_data(write, &version) + pumpkin_protocol::java::packet_encoder::write_packet(packet, &version, write) } pub fn serialize_packet_for_version( packet: &P, version: JavaMinecraftVersion, ) -> Result { - let mut packet_buf = Vec::new(); + pumpkin_protocol::java::packet_encoder::serialize_packet(packet, &version) + } - Self::write_packet_for_version(packet, version, &mut packet_buf)?; - Ok(packet_buf.into()) + pub fn serialize_packet(&self, packet: &P) -> Result { + Self::serialize_packet_for_version(packet, self.version.load()) + } + + pub async fn send_packet(&self, packet: &P) { + if let Ok(data) = self.serialize_packet(packet) { + self.send_packet_now(data).await; + } + } + + pub async fn enqueue_client_packet(&self, packet: &P) { + if let Ok(data) = self.serialize_packet(packet) { + self.enqueue_packet(data).await; + } } pub fn write_packet( @@ -761,9 +766,11 @@ impl JavaClient { id if id == pumpkin_protocol::java::server::play::SKeepAlive::to_id(version) => { self.handle_keep_alive( player, - pumpkin_protocol::java::server::play::SKeepAlive::read(&mut payload, &version)?, - ) - .await; + &pumpkin_protocol::java::server::play::SKeepAlive::read( + &mut payload, + &version, + )?, + ); } id if id == SClientTickEnd::to_id(version) => { // TODO diff --git a/crates/pumpkin/src/net/java/pending.rs b/crates/pumpkin/src/net/java/pending.rs index b96939311..802963359 100644 --- a/crates/pumpkin/src/net/java/pending.rs +++ b/crates/pumpkin/src/net/java/pending.rs @@ -33,7 +33,10 @@ use tracing::{debug, error, warn}; use crate::{ entity::player::ChatMode, - net::{EncryptionError, GameProfile, PacketHandlerResult, PlayerConfig, can_not_join}, + net::{ + EncryptionError, GameProfile, PacketHandlerResult, PacketRateLimiter, PlayerConfig, + can_not_join, + }, server::Server, }; @@ -53,11 +56,17 @@ pub struct PendingConnection { pub gameprofile: Option, pub config: Option, pub brand: Option, + pub packet_limiter: PacketRateLimiter, } impl PendingConnection { #[must_use] - pub fn new(tcp_stream: TcpStream, address: SocketAddr, id: u64) -> Self { + pub fn new( + tcp_stream: TcpStream, + address: SocketAddr, + id: u64, + packet_limiter: PacketRateLimiter, + ) -> Self { let (read, write) = tcp_stream.into_split(); Self { id, @@ -71,6 +80,7 @@ impl PendingConnection { gameprofile: None, config: None, brand: None, + packet_limiter, } } @@ -172,6 +182,25 @@ impl PendingConnection { pub async fn handle_login_sequence(&mut self, server: &Arc) -> PacketHandlerResult { while let Some(packet) = self.get_packet().await { + if !self.packet_limiter.check_packet() { + warn!( + "Pending client {} exceeded packet rate limit (rate: {}/s)", + self.id, + self.packet_limiter.max_rate() + ); + self.kick(TextComponent::text( + server + .advanced_config + .networking + .java + .packet_limiter + .kick_message + .clone(), + )) + .await; + return PacketHandlerResult::Stop; + } + match self.handle_packet(server, &packet).await { Ok(result) => { if let Some(result) = result { diff --git a/crates/pumpkin/src/net/java/play/command_suggestion.rs b/crates/pumpkin/src/net/java/play/command_suggestion.rs index 476602d80..7be007735 100644 --- a/crates/pumpkin/src/net/java/play/command_suggestion.rs +++ b/crates/pumpkin/src/net/java/play/command_suggestion.rs @@ -31,6 +31,6 @@ impl JavaClient { suggestions.into(), ); - self.enqueue_packet(&response).await; + self.enqueue_client_packet(&response).await; } } diff --git a/crates/pumpkin/src/net/java/play/interact.rs b/crates/pumpkin/src/net/java/play/interact.rs index 9a41b7948..276eb3aba 100644 --- a/crates/pumpkin/src/net/java/play/interact.rs +++ b/crates/pumpkin/src/net/java/play/interact.rs @@ -36,10 +36,7 @@ impl JavaClient { if let Some(target) = target { if player.gamemode.load() == GameMode::Spectator { player.camera_target_id.store(Some(entity_id.0)); - player - .client - .send_packet_now(&CSetCamera::new(entity_id)) - .await; + player.send_client_packet(&CSetCamera::new(entity_id)).await; return; } send_cancellable! {{ diff --git a/crates/pumpkin/src/net/java/play/keep_alive.rs b/crates/pumpkin/src/net/java/play/keep_alive.rs index 3f3c53fd9..f49097af4 100644 --- a/crates/pumpkin/src/net/java/play/keep_alive.rs +++ b/crates/pumpkin/src/net/java/play/keep_alive.rs @@ -2,23 +2,35 @@ use super::*; impl JavaClient { - pub async fn handle_keep_alive(&self, player: &Player, keep_alive: SKeepAlive) { - if self.wait_for_keep_alive.load(Ordering::Relaxed) - && keep_alive.keep_alive_id == self.keep_alive_id.load() + pub fn handle_keep_alive(&self, player: &Player, keep_alive: &SKeepAlive) { + let mut pending = self + .pending_keep_alives + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + if let Some(pos) = pending + .iter() + .position(|(id, _)| *id == keep_alive.keep_alive_id) { - let ping = self.last_keep_alive_time.load().elapsed(); + let (_, send_time) = pending.swap_remove(pos); + let ping = send_time.elapsed().as_millis() as u32; // Vanilla logic player.ping.store( - (player.ping.load(Ordering::Relaxed) * 3 + ping.as_millis() as u32) / 4, + (player.ping.load(Ordering::Relaxed) * 3 + ping) / 4, + Ordering::Relaxed, + ); + self.wait_for_keep_alive.store(false, Ordering::Relaxed); + } else if keep_alive.keep_alive_id == self.keep_alive_id.load() { + let ping = self.last_keep_alive_time.load().elapsed().as_millis() as u32; + player.ping.store( + (player.ping.load(Ordering::Relaxed) * 3 + ping) / 4, Ordering::Relaxed, ); self.wait_for_keep_alive.store(false, Ordering::Relaxed); } else { - self.kick(pumpkin_macros::translate_cross!( - translation::java::DISCONNECT_TIMEOUT, - translation::bedrock::DISCONNECT_TIMEOUT - )) - .await; + debug!( + "Ignored unexpected or duplicate keep alive id {} from player {}", + keep_alive.keep_alive_id, player.gameprofile.name + ); } } } diff --git a/crates/pumpkin/src/net/java/play/pick_item.rs b/crates/pumpkin/src/net/java/play/pick_item.rs index d176ca969..1ac03ab0f 100644 --- a/crates/pumpkin/src/net/java/play/pick_item.rs +++ b/crates/pumpkin/src/net/java/play/pick_item.rs @@ -40,8 +40,7 @@ impl JavaClient { } player - .client - .enqueue_packet(&CSetSelectedSlot::new( + .send_client_packet(&CSetSelectedSlot::new( player.inventory.get_selected_slot() as i8 )) .await; @@ -104,8 +103,7 @@ impl JavaClient { } player - .client - .enqueue_packet(&CSetSelectedSlot::new( + .send_client_packet(&CSetSelectedSlot::new( player.inventory.get_selected_slot() as i8 )) .await; diff --git a/crates/pumpkin/src/net/java/play/ping_request.rs b/crates/pumpkin/src/net/java/play/ping_request.rs index d7293d591..ada98ff15 100644 --- a/crates/pumpkin/src/net/java/play/ping_request.rs +++ b/crates/pumpkin/src/net/java/play/ping_request.rs @@ -3,7 +3,7 @@ use super::*; impl JavaClient { pub async fn handle_play_ping_request(&self, request: SPlayPingRequest) { - self.enqueue_packet(&CPingResponse::new(request.payload)) + self.enqueue_client_packet(&CPingResponse::new(request.payload)) .await; } } diff --git a/crates/pumpkin/src/net/java/play/player_action.rs b/crates/pumpkin/src/net/java/play/player_action.rs index 18d96288c..4b6c22ab3 100644 --- a/crates/pumpkin/src/net/java/play/player_action.rs +++ b/crates/pumpkin/src/net/java/play/player_action.rs @@ -66,7 +66,7 @@ impl JavaClient { let inventory = player.inventory(); let held = inventory.held_item().await; if !server.item_registry.can_mine(held.item, player) { - self.enqueue_packet(&CBlockUpdate::new( + self.enqueue_client_packet(&CBlockUpdate::new( position, VarInt(i32::from(state.id.as_u16())), )) @@ -277,7 +277,7 @@ impl JavaClient { async fn sync_block_state_to_client(&self, world: &World, position: BlockPos) { let synced_state_id = world.get_block_state_id(&position); - self.send_packet_now(&CBlockUpdate::new( + self.send_packet(&CBlockUpdate::new( position, VarInt(i32::from(synced_state_id.as_u16())), )) diff --git a/crates/pumpkin/src/net/java/play/player_input.rs b/crates/pumpkin/src/net/java/play/player_input.rs index 6cc46b612..45e23622a 100644 --- a/crates/pumpkin/src/net/java/play/player_input.rs +++ b/crates/pumpkin/src/net/java/play/player_input.rs @@ -27,8 +27,7 @@ impl JavaClient { { player.camera_target_id.store(None); player - .client - .send_packet_now(&CSetCamera::new(player.entity_id().into())) + .send_client_packet(&CSetCamera::new(player.entity_id().into())) .await; } diff --git a/crates/pumpkin/src/net/java/play/player_position.rs b/crates/pumpkin/src/net/java/play/player_position.rs index 5fcac7063..04f59bc7f 100644 --- a/crates/pumpkin/src/net/java/play/player_position.rs +++ b/crates/pumpkin/src/net/java/play/player_position.rs @@ -334,7 +334,7 @@ impl JavaClient { pub async fn force_tp(&self, player: &Arc, position: Vector3) { let teleport_id = player.teleport_id_count.fetch_add(1, Ordering::Relaxed) + 1; *player.awaiting_teleport.lock().await = Some((teleport_id.into(), position)); - self.enqueue_packet(&CPlayerPosition::new( + self.enqueue_client_packet(&CPlayerPosition::new( teleport_id.into(), player.get_entity().pos.load(), Vector3::new(0.0, 0.0, 0.0), diff --git a/crates/pumpkin/src/net/java/play/set_held_item.rs b/crates/pumpkin/src/net/java/play/set_held_item.rs index 9fe5ece70..c4637f469 100644 --- a/crates/pumpkin/src/net/java/play/set_held_item.rs +++ b/crates/pumpkin/src/net/java/play/set_held_item.rs @@ -24,8 +24,7 @@ impl JavaClient { server.plugin_manager.fire(server, &mut event).await; if event.cancelled { player - .client - .enqueue_packet(&CSetSelectedSlot::new(previous_slot as i8)) + .send_client_packet(&CSetSelectedSlot::new(previous_slot as i8)) .await; return; } diff --git a/crates/pumpkin/src/net/java/play/spectate_entity.rs b/crates/pumpkin/src/net/java/play/spectate_entity.rs index 944fa9a2d..43a7ec84b 100644 --- a/crates/pumpkin/src/net/java/play/spectate_entity.rs +++ b/crates/pumpkin/src/net/java/play/spectate_entity.rs @@ -28,8 +28,7 @@ impl JavaClient { player.camera_target_id.store(Some(target_id)); player - .client - .send_packet_now(&CSetCamera::new(target_id.into())) + .send_client_packet(&CSetCamera::new(target_id.into())) .await; player @@ -43,8 +42,7 @@ impl JavaClient { player.camera_target_id.store(Some(target_id)); player - .client - .send_packet_now(&CSetCamera::new(target_id.into())) + .send_client_packet(&CSetCamera::new(target_id.into())) .await; player diff --git a/crates/pumpkin/src/net/java/play/tag_query.rs b/crates/pumpkin/src/net/java/play/tag_query.rs index 570d7b842..5a25cc5cf 100644 --- a/crates/pumpkin/src/net/java/play/tag_query.rs +++ b/crates/pumpkin/src/net/java/play/tag_query.rs @@ -17,7 +17,7 @@ impl JavaClient { // Empty NBT compound (TAG_End = 0) let empty_nbt = [0u8]; - self.send_packet_now(&CTagQueryResponse::new(packet.transaction_id, &empty_nbt)) + self.send_packet(&CTagQueryResponse::new(packet.transaction_id, &empty_nbt)) .await; } @@ -28,7 +28,7 @@ impl JavaClient { // Empty NBT compound (TAG_End = 0) let empty_nbt = [0u8]; - self.send_packet_now(&CTagQueryResponse::new(packet.transaction_id, &empty_nbt)) + self.send_packet(&CTagQueryResponse::new(packet.transaction_id, &empty_nbt)) .await; } } diff --git a/crates/pumpkin/src/net/java/play/teleport_to_entity.rs b/crates/pumpkin/src/net/java/play/teleport_to_entity.rs index 1c38bcdd9..95234cfdf 100644 --- a/crates/pumpkin/src/net/java/play/teleport_to_entity.rs +++ b/crates/pumpkin/src/net/java/play/teleport_to_entity.rs @@ -25,8 +25,7 @@ impl JavaClient { let target_id = target_player.living_entity.entity.entity_id; player.camera_target_id.store(Some(target_id)); player - .client - .send_packet_now(&CSetCamera::new(target_id.into())) + .send_client_packet(&CSetCamera::new(target_id.into())) .await; player diff --git a/crates/pumpkin/src/net/java/play/use_item_on.rs b/crates/pumpkin/src/net/java/play/use_item_on.rs index 42ec6feea..00952241d 100644 --- a/crates/pumpkin/src/net/java/play/use_item_on.rs +++ b/crates/pumpkin/src/net/java/play/use_item_on.rs @@ -66,7 +66,7 @@ impl JavaClient { event; 'cancelled: { let state_id = world.get_block_state_id(&position); - self.enqueue_packet(&CBlockUpdate::new( + self.enqueue_client_packet(&CBlockUpdate::new( position, VarInt(i32::from(state_id.as_u16())), )) @@ -237,7 +237,7 @@ impl JavaClient { .await { Ok(Some((final_block_pos, new_state))) => { - self.send_packet_now(&CBlockUpdate::new( + self.send_packet(&CBlockUpdate::new( final_block_pos, VarInt(i32::from(new_state.as_u16())), )) @@ -256,7 +256,7 @@ impl JavaClient { /// Checks if the block placed was a sign, then opens a dialog. pub async fn send_sign_packet(&self, block_position: BlockPos, is_front_text: bool) { - self.enqueue_packet(&COpenSignEditor::new(block_position, is_front_text)) + self.enqueue_client_packet(&COpenSignEditor::new(block_position, is_front_text)) .await; } } diff --git a/crates/pumpkin/src/net/mod.rs b/crates/pumpkin/src/net/mod.rs index f0e227383..7375fc261 100644 --- a/crates/pumpkin/src/net/mod.rs +++ b/crates/pumpkin/src/net/mod.rs @@ -30,6 +30,8 @@ pub mod authentication; pub mod bedrock; pub mod java; pub mod lan_broadcast; +pub mod packet_limiter; +pub use packet_limiter::PacketRateLimiter; mod proxy; pub mod query; pub mod rcon; @@ -202,8 +204,16 @@ impl ClientPlatform { be_packet: &B, ) { match self { - Self::Java(java) => java.enqueue_packet(je_packet).await, - Self::Bedrock(bedrock) => bedrock.enqueue_packet(be_packet).await, + Self::Java(java) => { + if let Ok(data) = java.serialize_packet(je_packet) { + java.enqueue_packet(data).await; + } + } + Self::Bedrock(bedrock) => { + if let Ok(data) = bedrock.serialize_packet(be_packet) { + bedrock.enqueue_packet(data).await; + } + } } } @@ -213,38 +223,41 @@ impl ClientPlatform { be_packet: &B, ) { match self { - Self::Java(java) => java.try_enqueue_packet(je_packet), - Self::Bedrock(bedrock) => bedrock.try_enqueue_packet(be_packet), + Self::Java(java) => { + if let Ok(data) = java.serialize_packet(je_packet) { + java.try_enqueue_packet(data); + } + } + Self::Bedrock(bedrock) => { + if let Ok(data) = bedrock.serialize_packet(be_packet) { + bedrock.try_enqueue_packet(data); + } + } } } - pub async fn enqueue_packet(&self, packet: &P) { - if let Self::Java(java) = self { - java.enqueue_packet(packet).await; + pub async fn enqueue_packet(&self, packet_data: Bytes) { + match self { + Self::Java(java) => java.enqueue_packet(packet_data).await, + Self::Bedrock(bedrock) => bedrock.enqueue_packet(packet_data).await, } } - pub async fn enqueue_be_packet(&self, packet: &P) { - if let Self::Bedrock(bedrock) = self { - bedrock.enqueue_packet(packet).await; - } - } - - pub fn try_enqueue_packet(&self, packet: &P) { - if let Self::Java(java) = self { - java.try_enqueue_packet(packet); - } - } - - pub fn try_enqueue_be_packet(&self, packet: &P) { - if let Self::Bedrock(bedrock) = self { - bedrock.try_enqueue_packet(packet); + pub fn try_enqueue_packet(&self, packet_data: Bytes) { + match self { + Self::Java(java) => java.try_enqueue_packet(packet_data), + Self::Bedrock(bedrock) => bedrock.try_enqueue_packet(packet_data), } } pub fn try_enqueue_spawn_packet(&self, entity: &Arc) { match self { - Self::Java(java) => java.try_enqueue_packet(&entity.get_entity().create_spawn_packet()), + Self::Java(java) => { + let packet = entity.get_entity().create_spawn_packet(); + if let Ok(data) = java.serialize_packet(&packet) { + java.try_enqueue_packet(data); + } + } Self::Bedrock(bedrock) => bedrock.enqueue_spawn_packet(entity.clone()), } } @@ -263,15 +276,10 @@ impl ClientPlatform { } } - pub async fn send_packet_now(&self, packet: &P) { - if let Self::Java(java) = self { - java.send_packet_now(packet).await; - } - } - - pub async fn send_be_packet_now(&self, packet: &P) { - if let Self::Bedrock(bedrock) = self { - bedrock.send_game_packet(packet).await; + pub async fn send_packet_now(&self, packet_data: Bytes) { + match self { + Self::Java(java) => java.send_packet_now(packet_data).await, + Self::Bedrock(bedrock) => bedrock.send_game_packet(packet_data).await, } } @@ -281,16 +289,21 @@ impl ClientPlatform { be_packet: &B, ) { match self { - Self::Java(java) => java.send_packet_now(je_packet).await, - Self::Bedrock(bedrock) => bedrock.send_game_packet(be_packet).await, + Self::Java(java) => { + if let Ok(data) = java.serialize_packet(je_packet) { + java.send_packet_now(data).await; + } + } + Self::Bedrock(bedrock) => { + if let Ok(data) = bedrock.serialize_packet(be_packet) { + bedrock.send_game_packet(data).await; + } + } } } pub async fn send_packet_now_data(&self, data: Bytes) { - match self { - Self::Java(java) => java.send_packet_now_data(data).await, - Self::Bedrock(bedrock) => bedrock.enqueue_packet_data(data).await, - } + self.send_packet_now(data).await; } pub async fn kick(&self, reason: DisconnectReason, message: TextComponent) { diff --git a/crates/pumpkin/src/net/packet_limiter.rs b/crates/pumpkin/src/net/packet_limiter.rs new file mode 100644 index 000000000..3c9f91b25 --- /dev/null +++ b/crates/pumpkin/src/net/packet_limiter.rs @@ -0,0 +1,111 @@ +use std::sync::Mutex; +use std::time::Instant; + +use pumpkin_config::PacketLimiterConfig; + +#[derive(Debug)] +struct LimiterState { + tokens: f64, + last_update: Instant, +} + +/// Token bucket based rate limiter for incoming client packets. +#[derive(Debug)] +pub struct PacketRateLimiter { + enabled: bool, + max_rate: f64, + burst_capacity: f64, + state: Mutex, +} + +impl PacketRateLimiter { + #[must_use] + pub fn new(enabled: bool, max_rate: f64, burst_capacity: f64) -> Self { + Self { + enabled, + max_rate, + burst_capacity, + state: Mutex::new(LimiterState { + tokens: burst_capacity, + last_update: Instant::now(), + }), + } + } + + #[must_use] + pub fn from_config(config: &PacketLimiterConfig) -> Self { + Self::new( + config.enabled, + config.max_packet_rate, + config.burst_capacity, + ) + } + + /// Checks whether an incoming packet is allowed under the rate limit. + /// + /// Returns `true` if the packet is within limits, or `false` if the rate limit is exceeded. + #[must_use] + pub fn check_packet(&self) -> bool { + if !self.enabled || self.max_rate <= 0.0 { + return true; + } + + let now = Instant::now(); + let mut state = self + .state + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + let elapsed = now.duration_since(state.last_update).as_secs_f64(); + state.last_update = now; + + state.tokens = (state.tokens + elapsed * self.max_rate).min(self.burst_capacity); + + if state.tokens >= 1.0 { + state.tokens -= 1.0; + true + } else { + false + } + } + + #[must_use] + pub const fn max_rate(&self) -> f64 { + self.max_rate + } + + #[must_use] + pub const fn is_enabled(&self) -> bool { + self.enabled + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn limiter_allows_packets_within_capacity() { + let limiter = PacketRateLimiter::new(true, 10.0, 5.0); + for _ in 0..5 { + assert!(limiter.check_packet()); + } + // Burst exhausted + assert!(!limiter.check_packet()); + } + + #[test] + fn limiter_disabled() { + let limiter = PacketRateLimiter::new(false, 10.0, 1.0); + for _ in 0..100 { + assert!(limiter.check_packet()); + } + } + + #[test] + fn limiter_zero_rate() { + let limiter = PacketRateLimiter::new(true, 0.0, 1.0); + for _ in 0..100 { + assert!(limiter.check_packet()); + } + } +} diff --git a/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/player.rs b/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/player.rs index 1766ef03c..debebed83 100644 --- a/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/player.rs +++ b/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/player.rs @@ -1113,7 +1113,7 @@ impl pumpkin::plugin::player::HostPlayer for PluginHostState { // Sync to client let stack_serializer = ItemStackSerializer::from(stack); let packet = CSetContainerSlot::new(0, 0, slot as i16, &stack_serializer); - player.client.enqueue_packet(&packet).await; + player.send_client_packet(&packet).await; Ok(()) } @@ -1139,7 +1139,7 @@ impl pumpkin::plugin::player::HostPlayer for PluginHostState { // Sync to client let stack_serializer = ItemStackSerializer::from(stack); let packet = CSetContainerSlot::new(0, 0, slot as i16, &stack_serializer); - player.client.enqueue_packet(&packet).await; + player.send_client_packet(&packet).await; Ok(()) } @@ -1204,7 +1204,7 @@ impl pumpkin::plugin::player::HostPlayer for PluginHostState { let sync_id = handler.sync_id(); let stack_serializer = ItemStackSerializer::from(stack); let packet = CSetContainerSlot::new(sync_id as i8, 0, slot as i16, &stack_serializer); - player.client.enqueue_packet(&packet).await; + player.send_client_packet(&packet).await; } Ok(()) @@ -1229,7 +1229,7 @@ impl pumpkin::plugin::player::HostPlayer for PluginHostState { for slot in 0..27 { let packet = CSetContainerSlot::new(sync_id as i8, 0, slot as i16, &empty_serializer); - player.client.enqueue_packet(&packet).await; + player.send_client_packet(&packet).await; } } @@ -2215,7 +2215,7 @@ impl pumpkin::plugin::player::HostPlayer for PluginHostState { let player = player_from_resource(self, &player)?; if let crate::net::ClientPlatform::Java(client) = player.client.as_ref() { client - .send_packet_now(&pumpkin_protocol::java::client::play::CTransfer::new( + .send_packet(&pumpkin_protocol::java::client::play::CTransfer::new( &host, pumpkin_protocol::codec::var_int::VarInt(i32::from(port)), )) @@ -2946,8 +2946,7 @@ impl pumpkin::plugin::player::HostJavaPlayer for PluginHostState { if let crate::net::ClientPlatform::Java(_) = player.client.as_ref() { player - .client - .send_packet_now(&pumpkin_protocol::java::client::play::CCustomPayload::new( + .send_client_packet(&pumpkin_protocol::java::client::play::CCustomPayload::new( &channel, &data, )) .await; @@ -3098,7 +3097,7 @@ impl pumpkin::plugin::player::HostJavaPlayer for PluginHostState { match client.connection_state.load() { pumpkin_protocol::ConnectionState::Config => { client - .send_packet_now( + .send_packet( &pumpkin_protocol::java::client::config::CConfigShowDialog::new( pumpkin_protocol::IdOr::Value(DialogNBT::from_dialog( &protocol_dialog, @@ -3109,13 +3108,9 @@ impl pumpkin::plugin::player::HostJavaPlayer for PluginHostState { } pumpkin_protocol::ConnectionState::Play => { client - .send_packet_now( - &pumpkin_protocol::java::client::play::CPlayShowDialog::new( - pumpkin_protocol::IdOr::Value(DialogNBT::from_dialog( - &protocol_dialog, - )), - ), - ) + .send_packet(&pumpkin_protocol::java::client::play::CPlayShowDialog::new( + pumpkin_protocol::IdOr::Value(DialogNBT::from_dialog(&protocol_dialog)), + )) .await; } _ => {} @@ -3142,16 +3137,14 @@ impl pumpkin::plugin::player::HostJavaPlayer for PluginHostState { match client.connection_state.load() { pumpkin_protocol::ConnectionState::Config => { client - .send_packet_now( + .send_packet( &pumpkin_protocol::java::client::config::CConfigClearDialog::new(), ) .await; } pumpkin_protocol::ConnectionState::Play => { client - .send_packet_now( - &pumpkin_protocol::java::client::play::CPlayClearDialog::new(), - ) + .send_packet(&pumpkin_protocol::java::client::play::CPlayClearDialog::new()) .await; } _ => {} @@ -3216,7 +3209,7 @@ impl pumpkin::plugin::player::HostJavaPlayer for PluginHostState { if let crate::net::ClientPlatform::Java(client) = player.client.as_ref() { client - .send_packet_now( + .send_packet( &pumpkin_protocol::java::client::play::CAddResourcePack::new( &uuid, &pack.url, @@ -3248,7 +3241,7 @@ impl pumpkin::plugin::player::HostJavaPlayer for PluginHostState { if let crate::net::ClientPlatform::Java(client) = player.client.as_ref() { client - .send_packet_now( + .send_packet( &pumpkin_protocol::java::client::play::CRemoveResourcePack::new(Some(&uuid)), ) .await; @@ -3271,9 +3264,7 @@ impl pumpkin::plugin::player::HostJavaPlayer for PluginHostState { if let crate::net::ClientPlatform::Java(client) = player.client.as_ref() { client - .send_packet_now( - &pumpkin_protocol::java::client::play::CRemoveResourcePack::new(None), - ) + .send_packet(&pumpkin_protocol::java::client::play::CRemoveResourcePack::new(None)) .await; } Ok(()) @@ -3554,7 +3545,7 @@ impl pumpkin::plugin::player::HostBedrockPlayer for PluginHostState { }; if let crate::net::ClientPlatform::Bedrock(client) = player.client.as_ref() { - client.send_game_packet(&packet).await; + client.send_packet(&packet).await; } Ok(()) @@ -3658,7 +3649,7 @@ impl pumpkin::plugin::player::HostBedrockPlayer for PluginHostState { }; client - .send_game_packet(&CModalFormRequest { + .send_packet(&CModalFormRequest { form_id: pumpkin_protocol::codec::var_uint::VarUInt(form_id), form_data: form_json.to_string(), }) @@ -3750,7 +3741,7 @@ impl pumpkin::plugin::player::HostBedrockPlayer for PluginHostState { resource_packs: entries, }; - client.send_game_packet(&packs_info).await; + client.send_packet(&packs_info).await; } Ok(()) } diff --git a/crates/pumpkin/src/server/mod.rs b/crates/pumpkin/src/server/mod.rs index 9b6654a35..63d309426 100644 --- a/crates/pumpkin/src/server/mod.rs +++ b/crates/pumpkin/src/server/mod.rs @@ -741,8 +741,8 @@ impl Server { for player in world.players.load().iter() { *player.tab_list_header.lock().await = header.clone(); *player.tab_list_footer.lock().await = footer.clone(); - player.client.enqueue_packet(&packet).await; } + world.broadcast_packet_all(&packet); } } @@ -1063,13 +1063,15 @@ impl Server { pumpkin_protocol::codec::var_int::VarInt(0), ); for world in self.worlds.load().iter() { - for player in world.players.load().iter() { - if player.subscribed_debug_sample.load(Ordering::Relaxed) - && player.permission_lvl.load() >= pumpkin_util::PermissionLvl::Two - { - player.client.try_enqueue_packet(&packet); - } - } + let players = world.players.load(); + let recipients = players + .iter() + .filter(|player| { + player.subscribed_debug_sample.load(Ordering::Relaxed) + && player.permission_lvl.load() >= pumpkin_util::PermissionLvl::Two + }) + .filter_map(|player| player.client.java()); + World::broadcast_java_clients(&packet, recipients); } } diff --git a/crates/pumpkin/src/server/tick_rate_manager.rs b/crates/pumpkin/src/server/tick_rate_manager.rs index 96058ab85..6d799125c 100644 --- a/crates/pumpkin/src/server/tick_rate_manager.rs +++ b/crates/pumpkin/src/server/tick_rate_manager.rs @@ -211,12 +211,10 @@ impl ServerTickRateManager { pub async fn update_joining_player(&self, player: &Player) { if player.client.java_version() >= pumpkin_util::version::JavaMinecraftVersion::V_1_20_3 { player - .client - .send_packet_now(&CTickingState::new(self.tickrate(), self.is_frozen())) + .send_client_packet(&CTickingState::new(self.tickrate(), self.is_frozen())) .await; player - .client - .send_packet_now(&CTickingStep::new( + .send_client_packet(&CTickingStep::new( self.frozen_ticks_to_run.load(Ordering::Relaxed).into(), )) .await; diff --git a/crates/pumpkin/src/server/ticker.rs b/crates/pumpkin/src/server/ticker.rs index ba324b54d..8ac09e852 100644 --- a/crates/pumpkin/src/server/ticker.rs +++ b/crates/pumpkin/src/server/ticker.rs @@ -62,6 +62,9 @@ impl Ticker { next_tick += tick_interval; + // Explicitly yield to tokio to allow pending network packets / I/O tasks to be processed + tokio::task::yield_now().await; + tokio::select! { () = sleep_until(next_tick) => {}, () = STOP_INTERRUPT.cancelled() => { @@ -69,9 +72,11 @@ impl Ticker { } } - // Death Spiral Prevention + // Death Spiral Prevention / Catch-up Clamping + // If the server fell behind the scheduled tick, clamp next_tick to now + // so we don't run a burst of back-to-back ticks with zero sleep. let now = Instant::now(); - if now.saturating_duration_since(next_tick) > Duration::from_secs(5) { + if now > next_tick { next_tick = now; } } diff --git a/crates/pumpkin/src/world/border.rs b/crates/pumpkin/src/world/border.rs index adab97ec2..373b1c63d 100644 --- a/crates/pumpkin/src/world/border.rs +++ b/crates/pumpkin/src/world/border.rs @@ -46,7 +46,7 @@ impl Worldborder { pub async fn init_client(&self, client: &JavaClient) { client - .enqueue_packet(&CInitializeWorldBorder::new( + .enqueue_client_packet(&CInitializeWorldBorder::new( self.center_x, self.center_z, self.old_diameter, diff --git a/crates/pumpkin/src/world/bossbar.rs b/crates/pumpkin/src/world/bossbar.rs index 7fc70e27b..c7a2ad116 100644 --- a/crates/pumpkin/src/world/bossbar.rs +++ b/crates/pumpkin/src/world/bossbar.rs @@ -105,7 +105,7 @@ impl Player { }; let packet = CBossEvent::new(&bossbar.uuid, boss_action); - java.enqueue_packet(&packet).await; + java.enqueue_client_packet(&packet).await; } ClientPlatform::Bedrock(bedrock) => { let packet = BBossEvent { @@ -117,7 +117,7 @@ impl Player { overlay: bossbar.division.to_bedrock(), }, }; - bedrock.send_game_packet(&packet).await; + bedrock.send_packet(&packet).await; } } } @@ -128,14 +128,14 @@ impl Player { let boss_action = BosseventAction::Remove; let packet = CBossEvent::new(&uuid, boss_action); - java.enqueue_packet(&packet).await; + java.enqueue_client_packet(&packet).await; } ClientPlatform::Bedrock(bedrock) => { let packet = BBossEvent { boss_entity_id: VarLong(uuid.as_u128() as i64), action: BossEventAction::Remove, }; - bedrock.send_game_packet(&packet).await; + bedrock.send_packet(&packet).await; } } } @@ -146,14 +146,14 @@ impl Player { let boss_action = BosseventAction::UpdateHealth(health); let packet = CBossEvent::new(uuid, boss_action); - java.enqueue_packet(&packet).await; + java.enqueue_client_packet(&packet).await; } ClientPlatform::Bedrock(bedrock) => { let packet = BBossEvent { boss_entity_id: VarLong(uuid.as_u128() as i64), action: BossEventAction::UpdateHealth(health), }; - bedrock.send_game_packet(&packet).await; + bedrock.send_packet(&packet).await; } } } @@ -164,14 +164,14 @@ impl Player { let boss_action = BosseventAction::UpdateTile(title); let packet = CBossEvent::new(uuid, boss_action); - java.enqueue_packet(&packet).await; + java.enqueue_client_packet(&packet).await; } ClientPlatform::Bedrock(bedrock) => { let packet = BBossEvent { boss_entity_id: VarLong(uuid.as_u128() as i64), action: BossEventAction::UpdateTitle(title.get_text()), }; - bedrock.send_game_packet(&packet).await; + bedrock.send_packet(&packet).await; } } } @@ -191,7 +191,7 @@ impl Player { }; let packet = CBossEvent::new(uuid, boss_action); - java.enqueue_packet(&packet).await; + java.enqueue_client_packet(&packet).await; } ClientPlatform::Bedrock(bedrock) => { let packet = BBossEvent { @@ -201,7 +201,7 @@ impl Player { overlay: dividers.to_bedrock(), }, }; - bedrock.send_game_packet(&packet).await; + bedrock.send_packet(&packet).await; } } } @@ -212,7 +212,7 @@ impl Player { let boss_action = BosseventAction::UpdateFlags(flags.bits()); let packet = CBossEvent::new(uuid, boss_action); - java.enqueue_packet(&packet).await; + java.enqueue_client_packet(&packet).await; } ClientPlatform::Bedrock(bedrock) => { // For Bedrock, flags are part of properties (screen darken) @@ -227,7 +227,7 @@ impl Player { overlay: VarInt(0), }, }; - bedrock.send_game_packet(&packet).await; + bedrock.send_packet(&packet).await; } } } diff --git a/crates/pumpkin/src/world/chunker.rs b/crates/pumpkin/src/world/chunker.rs index 11a336ffb..9abbd9211 100644 --- a/crates/pumpkin/src/world/chunker.rs +++ b/crates/pumpkin/src/world/chunker.rs @@ -60,7 +60,7 @@ pub async fn update_position(player: &Arc) { match player.client.as_ref() { ClientPlatform::Java(java_client) => { java_client - .send_packet_now(&CCenterChunk { + .send_packet(&CCenterChunk { chunk_x: new_chunk_center.x.into(), chunk_z: new_chunk_center.y.into(), }) @@ -68,7 +68,7 @@ pub async fn update_position(player: &Arc) { } ClientPlatform::Bedrock(bedrock_client) => { bedrock_client - .send_game_packet(&CNetworkChunkPublisherUpdate::new( + .send_packet(&CNetworkChunkPublisherUpdate::new( player.get_entity().block_pos.load(), u32::from(view_distance.get()) * 16, )) @@ -100,7 +100,7 @@ pub async fn update_position(player: &Arc) { if let ClientPlatform::Java(client) = player.client.as_ref() { for chunk in &unloading_chunks { client - .enqueue_packet(&CUnloadChunk::new(chunk.x, chunk.y)) + .enqueue_client_packet(&CUnloadChunk::new(chunk.x, chunk.y)) .await; } } diff --git a/crates/pumpkin/src/world/mod.rs b/crates/pumpkin/src/world/mod.rs index de52142c4..d0a1b2e50 100644 --- a/crates/pumpkin/src/world/mod.rs +++ b/crates/pumpkin/src/world/mod.rs @@ -40,7 +40,7 @@ use crate::{ command::client_suggestions, entity::{Entity, EntityBase, NBTStorage, RemovalReason, player::Player, r#type::from_type}, error::PumpkinError, - net::{ClientPlatform, java::JavaClient}, + net::{ClientPlatform, bedrock::BedrockClient, java::JavaClient}, plugin::{ block::block_break::BlockBreakEvent, player::{ @@ -52,7 +52,7 @@ use crate::{ }; use arc_swap::ArcSwap; use border::Worldborder; -use bytes::BufMut; +use bytes::{BufMut, Bytes}; use explosion::Explosion; use pumpkin_config::BasicConfiguration; use pumpkin_data::block_properties::{blocks_movement, is_air}; @@ -661,7 +661,7 @@ impl World { } } - fn collect_java_recipients_by_version<'a>( + pub(crate) fn collect_java_recipients_by_version<'a>( players: impl Iterator>, ) -> BTreeMap> { let mut recipients_by_version: BTreeMap> = @@ -677,6 +677,21 @@ impl World { recipients_by_version } + pub fn broadcast_java_clients<'a, P: ClientPacket>( + packet: &P, + recipients: impl Iterator, + ) { + let mut recipients_by_version: BTreeMap> = + BTreeMap::new(); + for client in recipients { + recipients_by_version + .entry(client.version.load()) + .or_default() + .push(client); + } + Self::broadcast_java_grouped(packet, recipients_by_version); + } + fn broadcast_java_grouped( packet: &P, recipients_by_version: BTreeMap>, @@ -696,11 +711,55 @@ impl World { }; for recipient in recipients { - recipient.try_enqueue_packet_data(packet_data.clone()); + recipient.try_enqueue_packet(packet_data.clone()); } } } + fn broadcast_bedrock_grouped<'a, P: BClientPacket>( + packet: &P, + recipients: impl Iterator>, + ) { + let packet_data = match pumpkin_protocol::bedrock::packet_encoder::serialize_packet(packet) + { + Ok(packet_data) => packet_data, + Err(err) => { + error!( + "Failed to serialize bedrock packet {}: {}", + std::any::type_name::

(), + err + ); + return; + } + }; + + for recipient in recipients { + recipient.try_enqueue_packet(packet_data.clone()); + } + } + + async fn broadcast_bedrock_grouped_async<'a, P: BClientPacket>( + packet: &P, + recipients: impl Iterator>, + ) { + let packet_data = match pumpkin_protocol::bedrock::packet_encoder::serialize_packet(packet) + { + Ok(packet_data) => packet_data, + Err(err) => { + error!( + "Failed to serialize bedrock packet {}: {}", + std::any::type_name::

(), + err + ); + return; + } + }; + + for recipient in recipients { + recipient.enqueue_packet(packet_data.clone()).await; + } + } + /// Broadcasts a packet to all connected players within the world. /// Please avoid this as we want to replace it with `broadcast_editioned` /// @@ -715,20 +774,8 @@ impl World { pub fn broadcast_packet_all_sync(&self, packet: &P) { let players = self.players.load(); - for player in players.iter() { - match player.client.as_ref() { - ClientPlatform::Java(java) => { - if let Ok(data) = - JavaClient::serialize_packet_for_version(packet, java.version.load()) - { - java.try_enqueue_packet_data(data); - } - } - ClientPlatform::Bedrock(_) => { - // TODO - } - } - } + let recipients_by_version = Self::collect_java_recipients_by_version(players.iter()); + Self::broadcast_java_grouped(packet, recipients_by_version); } pub async fn broadcast_system_message(&self, message: &TextComponent, overlay: bool) { @@ -781,19 +828,16 @@ impl World { ) { let players = self.players.load(); let je_recipients_by_version = Self::collect_java_recipients_by_version(players.iter()); - let mut be_recipients = Vec::new(); - - for player in players.iter() { - if let ClientPlatform::Bedrock(be_client) = player.client.as_ref() { - be_recipients.push(be_client.clone()); - } - } Self::broadcast_java_grouped(je_packet, je_recipients_by_version); - - for recipient in be_recipients { - recipient.enqueue_packet(be_packet).await; - } + Self::broadcast_bedrock_grouped_async( + be_packet, + players.iter().filter_map(|p| match p.client.as_ref() { + ClientPlatform::Bedrock(be) => Some(be), + ClientPlatform::Java(_) => None, + }), + ) + .await; } pub async fn broadcast_secure_player_chat( @@ -825,7 +869,18 @@ impl World { TextComponent::empty(), // Not needed since we're injecting the name in the message for custom formatting None, ); - recipient.client.enqueue_packet(packet).await; + let packet_data = recipient.client.java().map_or_else( + || { + JavaClient::serialize_packet_for_version( + packet, + recipient.client.java_version(), + ) + }, + |j| j.serialize_packet(packet), + ); + if let Ok(data) = packet_data { + recipient.client.enqueue_packet(data).await; + } if let Some(signature) = chat_message.signature { recipient @@ -857,6 +912,7 @@ impl World { ) { 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) { @@ -864,13 +920,14 @@ impl World { } match p.client.as_ref() { ClientPlatform::Java(_) => java_recipients.push(p), - ClientPlatform::Bedrock(be_client) => be_client.try_enqueue_packet(be_packet), + ClientPlatform::Bedrock(be_client) => bedrock_recipients.push(be_client), } } let recipients_by_version = Self::collect_java_recipients_by_version(java_recipients.into_iter()); Self::broadcast_java_grouped(je_packet, recipients_by_version); + Self::broadcast_bedrock_grouped(be_packet, bedrock_recipients.into_iter()); } /// Broadcasts the skin layers of a player, encoding the metadata for each Java client's own @@ -882,36 +939,50 @@ impl World { skin_parts: u8, be_packet: &B, ) { - for p in self.players.load().iter() { + 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.as_ref() { - ClientPlatform::Java(client) => { - let version = client.version.load(); - if version < CURRENT_MC_VERSION { - continue; - } - let mut buf = Vec::new(); - for meta in [ - Metadata::new( - pumpkin_data::tracked_data::player::PLAYER_MODE_CUSTOMISATION, - skin_parts, - ), - Metadata::new( - pumpkin_data::tracked_data::player::PLAYER_MODE_CUSTOMIZATION_ID, - skin_parts, - ), - ] { - let _ = meta.write(&mut buf, &version); - } - buf.put_u8(255); - client - .try_enqueue_packet(&CSetEntityMetadata::new(entity_id.into(), buf.into())); - } - ClientPlatform::Bedrock(be_client) => be_client.try_enqueue_packet(be_packet), + ClientPlatform::Java(_) => java_recipients.push(p), + ClientPlatform::Bedrock(be_client) => bedrock_recipients.push(be_client), } } + + let recipients_by_version = + Self::collect_java_recipients_by_version(java_recipients.into_iter()); + + for (version, recipients) in recipients_by_version { + if version < CURRENT_MC_VERSION { + continue; + } + let mut buf = Vec::new(); + for meta in [ + Metadata::new( + pumpkin_data::tracked_data::player::PLAYER_MODE_CUSTOMISATION, + skin_parts, + ), + Metadata::new( + pumpkin_data::tracked_data::player::PLAYER_MODE_CUSTOMIZATION_ID, + skin_parts, + ), + ] { + let _ = meta.write(&mut buf, &version); + } + buf.put_u8(255); + let packet = CSetEntityMetadata::new(entity_id.into(), buf.into()); + if let Ok(packet_data) = JavaClient::serialize_packet_for_version(&packet, version) { + for recipient in recipients { + recipient.try_enqueue_packet(packet_data.clone()); + } + } + } + + Self::broadcast_bedrock_grouped(be_packet, bedrock_recipients.into_iter()); } pub async fn broadcast_packet_except_editioned( @@ -930,17 +1001,14 @@ impl World { } match p.client.as_ref() { ClientPlatform::Java(_) => java_recipients.push(p), - ClientPlatform::Bedrock(be_client) => bedrock_recipients.push(be_client.clone()), + ClientPlatform::Bedrock(be_client) => bedrock_recipients.push(be_client), } } 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; - } + Self::broadcast_bedrock_grouped_async(be_packet, bedrock_recipients.into_iter()).await; } /// Broadcasts a packet to all connected players within the world, excluding the specified players. @@ -1025,11 +1093,13 @@ impl World { }; let chunk_pos = BlockPos::floored_v(*position).chunk_position(); - for player in self.players.load().iter() { - if is_within_view_distance(chunk_pos, player.get_entity().chunk_pos.load(), 1) - && let ClientPlatform::Bedrock(client) = player.client.as_ref() - { - client.try_enqueue_packet(&packet); + if let Ok(data) = pumpkin_protocol::bedrock::packet_encoder::serialize_packet(&packet) { + for player in self.players.load().iter() { + if is_within_view_distance(chunk_pos, player.get_entity().chunk_pos.load(), 1) + && let ClientPlatform::Bedrock(client) = player.client.as_ref() + { + client.try_enqueue_packet(data.clone()); + } } } } @@ -1149,7 +1219,11 @@ impl World { let entity = player.get_entity(); let pos = entity.pos.load(); let bb = entity.bounding_box.load().expand(1.0, 0.5, 1.0); - (player.clone(), pos, bb) + let chunk_pos = Vector2::new( + get_section_cord(pos.x.floor() as i32), + get_section_cord(pos.z.floor() as i32), + ); + (player.clone(), pos, bb, chunk_pos) }) .collect::>(), ); @@ -1218,8 +1292,10 @@ impl World { let entity_pos = entity_inner.pos.load(); let entity_bb = entity_inner.bounding_box.load(); - for (player, player_pos, player_bb) in p_cache.iter() { - if (player_pos.x - entity_pos.x).abs() < 5.0 + for (player, player_pos, player_bb, player_chunk) in p_cache.iter() { + if (player_chunk.x - entity_chunk.x).abs() <= 1 + && (player_chunk.y - entity_chunk.y).abs() <= 1 + && (player_pos.x - entity_pos.x).abs() < 5.0 && (player_pos.y - entity_pos.y).abs() < 5.0 && (player_pos.z - entity_pos.z).abs() < 5.0 && player_bb.intersects(&entity_bb) @@ -1251,11 +1327,13 @@ impl World { let block_entity_future = async move { let t = tokio::time::Instant::now(); let mut tasks = tokio::task::JoinSet::new(); - for be in block_entities { - let be_clone = be.clone(); + for be_batch in block_entities.chunks(16) { + let batch = be_batch.to_vec(); let w_clone = world_for_be.clone(); tasks.spawn(async move { - be_clone.tick(&w_clone).await; + for be in batch { + be.tick(&w_clone).await; + } }); } while let Some(res) = tasks.join_next().await { @@ -1316,6 +1394,7 @@ impl World { } } + #[expect(clippy::too_many_lines)] pub async fn flush_block_updates(&self) { let mut block_state_updates_by_chunk_section: HashMap< Vector3, @@ -1367,30 +1446,47 @@ impl World { is_within_view_distance(chunk_pos, center, view_distance) }); + let mut bedrock_packets: Vec = Vec::new(); + for (block_pos, block_state_id) in &updates { + let be_block_id = BlockState::to_be_network_id(*block_state_id); + let update_packet = pumpkin_protocol::bedrock::client::CUpdateBlock::new( + *block_pos, + be_block_id as u32, + ); + if let Ok(data) = + pumpkin_protocol::bedrock::packet_encoder::serialize_packet(&update_packet) + { + bedrock_packets.push(data); + } + if let Some(data) = self.bedrock_block_entity_data(*block_state_id, *block_pos) + { + let actor_packet = CBlockActorData::new(*block_pos, data); + if let Ok(data) = + pumpkin_protocol::bedrock::packet_encoder::serialize_packet( + &actor_packet, + ) + { + bedrock_packets.push(data); + } + } + } + + let mut bedrock_recipients = Vec::new(); for p in recipients { match p.client.as_ref() { ClientPlatform::Java(_) => java_recipients.push(p), ClientPlatform::Bedrock(be_client) => { - for (block_pos, block_state_id) in &updates { - let be_block_id = BlockState::to_be_network_id(*block_state_id); - be_client.try_enqueue_packet( - &pumpkin_protocol::bedrock::client::CUpdateBlock::new( - *block_pos, - be_block_id as u32, - ), - ); - if let Some(data) = - self.bedrock_block_entity_data(*block_state_id, *block_pos) - { - be_client.try_enqueue_packet(&CBlockActorData::new( - *block_pos, data, - )); - } - } + bedrock_recipients.push(be_client); } } } + for be_client in bedrock_recipients { + for packet_data in &bedrock_packets { + be_client.try_enqueue_packet(packet_data.clone()); + } + } + let recipients_by_version = Self::collect_java_recipients_by_version(java_recipients.into_iter()); Self::broadcast_java_grouped( @@ -1399,25 +1495,34 @@ impl World { ); } - let players = self.players.load(); - let recipients = players.iter().filter(|player| { - let center = player.get_entity().chunk_pos.load(); - let view_distance = get_view_distance(player).get() as i32; - is_within_view_distance(chunk_pos, center, view_distance) - }); - for player in recipients { - let ClientPlatform::Bedrock(client) = player.client.as_ref() else { - continue; - }; - for (block_pos, block_state_id) in &updates { - let water_state = bedrock_water_state(*block_state_id); - client.try_enqueue_packet( - &pumpkin_protocol::bedrock::client::CUpdateBlock::with_layer( - *block_pos, - u32::from(BlockState::to_be_network_id(water_state)), - 1, - ), - ); + let mut bedrock_water_packets: Vec = Vec::new(); + for (block_pos, block_state_id) in &updates { + let water_state = bedrock_water_state(*block_state_id); + let packet = pumpkin_protocol::bedrock::client::CUpdateBlock::with_layer( + *block_pos, + u32::from(BlockState::to_be_network_id(water_state)), + 1, + ); + if let Ok(data) = + pumpkin_protocol::bedrock::packet_encoder::serialize_packet(&packet) + { + bedrock_water_packets.push(data); + } + } + + if !bedrock_water_packets.is_empty() { + let players = self.players.load(); + let recipients = players.iter().filter(|player| { + let center = player.get_entity().chunk_pos.load(); + let view_distance = get_view_distance(player).get() as i32; + is_within_view_distance(chunk_pos, center, view_distance) + }); + for player in recipients { + if let ClientPlatform::Bedrock(client) = player.client.as_ref() { + for packet_data in &bedrock_water_packets { + client.try_enqueue_packet(packet_data.clone()); + } + } } } } @@ -1486,6 +1591,8 @@ impl World { #[expect(clippy::too_many_lines)] pub async fn tick_chunks(self: &Arc) { + const BATCH_SIZE: usize = 32; + let active_chunks = self.active_chunks.load(); let tick_data = self.level.get_tick_data(&active_chunks); @@ -1493,69 +1600,78 @@ impl World { let mut chunk_tasks = tokio::task::JoinSet::new(); // 1. Spawn Block Ticks - for scheduled_tick in tick_data.block_ticks { + for chunk_batch in tick_data.block_ticks.chunks(BATCH_SIZE) { + let batch = chunk_batch.to_vec(); let world = self.clone(); - let pos = scheduled_tick.position; // Clone for the move closure chunk_tasks.spawn(async move { - let block = world.get_block(&pos); - if let Some(pumpkin_block) = world.block_registry.get_pumpkin_block(block.id) { - pumpkin_block - .on_scheduled_tick(OnScheduledTickArgs { - world: &world, - block, - position: &pos, - }) - .await; + for scheduled_tick in batch { + let pos = scheduled_tick.position; + let block = world.get_block(&pos); + if let Some(pumpkin_block) = world.block_registry.get_pumpkin_block(block.id) { + pumpkin_block + .on_scheduled_tick(OnScheduledTickArgs { + world: &world, + block, + position: &pos, + }) + .await; + } } }); } // 2. Spawn Fluid Ticks - for scheduled_tick in tick_data.fluid_ticks { + for chunk_batch in tick_data.fluid_ticks.chunks(BATCH_SIZE) { + let batch = chunk_batch.to_vec(); let world = self.clone(); - let pos = scheduled_tick.position; chunk_tasks.spawn(async move { - let fluid = world.get_fluid(&pos); - if let Some(pumpkin_fluid) = world.block_registry.get_pumpkin_fluid(fluid.id) { - pumpkin_fluid.on_scheduled_tick(&world, fluid, &pos).await; + for scheduled_tick in batch { + let pos = scheduled_tick.position; + let fluid = world.get_fluid(&pos); + if let Some(pumpkin_fluid) = world.block_registry.get_pumpkin_fluid(fluid.id) { + pumpkin_fluid.on_scheduled_tick(&world, fluid, &pos).await; + } } }); } // 3. Spawn Random Ticks - for scheduled_tick in tick_data.random_ticks { + for chunk_batch in tick_data.random_ticks.chunks(BATCH_SIZE) { + let batch = chunk_batch.to_vec(); let world = self.clone(); - let pos = scheduled_tick.position; - let tick_block = scheduled_tick.tick_block; - let tick_fluid = scheduled_tick.tick_fluid; - chunk_tasks.spawn(async move { - let (block, fluid) = match (tick_block, tick_fluid) { - (true, true) => { - let (b, f) = world.get_block_and_fluid(&pos); - (Some(b), Some(f)) + for scheduled_tick in batch { + let pos = scheduled_tick.position; + let (block, fluid) = + match (scheduled_tick.tick_block, scheduled_tick.tick_fluid) { + (true, true) => { + let (b, f) = world.get_block_and_fluid(&pos); + (Some(b), Some(f)) + } + (true, false) => (Some(world.get_block(&pos)), None), + (false, true) => (None, Some(world.get_fluid(&pos))), + (false, false) => (None, None), + }; + + if let Some(block) = block + && let Some(pumpkin_block) = + world.block_registry.get_pumpkin_block(block.id) + { + pumpkin_block + .random_tick(RandomTickArgs { + world: &world, + block, + position: &pos, + }) + .await; } - (true, false) => (Some(world.get_block(&pos)), None), - (false, true) => (None, Some(world.get_fluid(&pos))), - (false, false) => (None, None), - }; - if let Some(block) = block - && let Some(pumpkin_block) = world.block_registry.get_pumpkin_block(block.id) - { - pumpkin_block - .random_tick(RandomTickArgs { - world: &world, - block, - position: &pos, - }) - .await; - } - - if let Some(fluid) = fluid - && let Some(pumpkin_fluid) = world.block_registry.get_pumpkin_fluid(fluid.id) - { - pumpkin_fluid.random_tick(fluid, &world, &pos).await; + if let Some(fluid) = fluid + && let Some(pumpkin_fluid) = + world.block_registry.get_pumpkin_fluid(fluid.id) + { + pumpkin_fluid.random_tick(fluid, &world, &pos).await; + } } }); } @@ -1592,15 +1708,18 @@ impl World { spawning_chunks.shuffle(&mut rng()); - for (pos, chunk) in spawning_chunks { + for chunk_batch in spawning_chunks.chunks(8) { + let batch = chunk_batch.to_vec(); let world = self.clone(); let s_list = spawn_list.clone(); let s_state = spawn_state.clone(); chunk_tasks.spawn(async move { - world - .tick_spawning_chunk(pos, &chunk, &s_list, &s_state) - .await; + for (pos, chunk) in batch { + world + .tick_spawning_chunk(pos, &chunk, &s_list, &s_state) + .await; + } }); } } @@ -2257,71 +2376,75 @@ impl World { return; }; - client - .send_game_packet(&CStartGame { - entity_id: VarLong(runtime_id as _), - runtime_entity_id: VarULong(runtime_id), - player_gamemode: player.gamemode.load(), - // Bedrock represents the local player at eye height; Pumpkin stores feet position. - position: Vector3::new( - position.x as f32, - position.y as f32 + player.get_entity().entity_type.eye_height, - position.z as f32, - ), - pitch, - yaw, - level_settings, - level_id: String::new(), - level_name: "Pumpkin world".to_string(), - premium_world_template_id: String::new(), - is_trial: false, - rewind_history_size: VarInt(0), - server_authoritative_block_breaking: true, - current_level_time: self.level_time.lock().await.world_age as _, - enchantment_seed: VarInt(0), - block_properties_size: VarUInt(0), - // TODO Make this unique - multiplayer_correlation_id: Uuid::default().to_string(), - enable_itemstack_net_manager: true, - server_version: "Pumpkin Rust Server".to_string(), - compound_id: 10, - compound_len: VarUInt(0), - compound_end: 0, - block_registry_checksum: 0, - world_template_id: Uuid::nil(), - enable_clientside_generation: false, - blocknetwork_ids_are_hashed: false, - server_auth_sounds: true, - server_join_information: None, - telemetry: ServerTelemetryData { - server_id: String::new(), - scenario_id: String::new(), - world_id: String::new(), - owner_id: String::new(), - }, - }) - .await; + let start_game = CStartGame { + entity_id: VarLong(runtime_id as _), + runtime_entity_id: VarULong(runtime_id), + player_gamemode: player.gamemode.load(), + // Bedrock represents the local player at eye height; Pumpkin stores feet position. + position: Vector3::new( + position.x as f32, + position.y as f32 + player.get_entity().entity_type.eye_height, + position.z as f32, + ), + pitch, + yaw, + level_settings, + level_id: String::new(), + level_name: "Pumpkin world".to_string(), + premium_world_template_id: String::new(), + is_trial: false, + rewind_history_size: VarInt(0), + server_authoritative_block_breaking: true, + current_level_time: self.level_time.lock().await.world_age as _, + enchantment_seed: VarInt(0), + block_properties_size: VarUInt(0), + // TODO Make this unique + multiplayer_correlation_id: Uuid::default().to_string(), + enable_itemstack_net_manager: true, + server_version: "Pumpkin Rust Server".to_string(), + compound_id: 10, + compound_len: VarUInt(0), + compound_end: 0, + block_registry_checksum: 0, + world_template_id: Uuid::nil(), + enable_clientside_generation: false, + blocknetwork_ids_are_hashed: false, + server_auth_sounds: true, + server_join_information: None, + telemetry: ServerTelemetryData { + server_id: String::new(), + scenario_id: String::new(), + world_id: String::new(), + owner_id: String::new(), + }, + }; + if let Ok(data) = client.serialize_packet(&start_game) { + client.send_game_packet(data).await; + } - client.send_game_packet(&CBiomeDefinitionList).await; + if let Ok(data) = client.serialize_packet(&CBiomeDefinitionList) { + client.send_game_packet(data).await; + } - client - .send_game_packet(&CItemRegistry { - items: BedrockItem::ALL_BEDROCK_ITEMS - .iter() - .map(|b| ItemDefinition { - name: b.registry_key.into(), - id: b.id, - component_based: b.component_based, - item_version: VarInt::from(match b.version { - BedrockItemVersion::Legacy => 0, - BedrockItemVersion::DataDriven => 1, - BedrockItemVersion::None => 2, - }), - component_data: b.definition_components.into(), - }) - .collect::>(), - }) - .await; + let item_registry = CItemRegistry { + items: BedrockItem::ALL_BEDROCK_ITEMS + .iter() + .map(|b| ItemDefinition { + name: b.registry_key.into(), + id: b.id, + component_based: b.component_based, + item_version: VarInt::from(match b.version { + BedrockItemVersion::Legacy => 0, + BedrockItemVersion::DataDriven => 1, + BedrockItemVersion::None => 2, + }), + component_data: b.definition_components.into(), + }) + .collect::>(), + }; + if let Ok(data) = client.serialize_packet(&item_registry) { + client.send_game_packet(data).await; + } let (groups, entries) = CREATIVE_CONTENT.get_or_init(|| { let groups = pumpkin_data::bedrock_creative::CREATIVE_GROUPS @@ -2379,10 +2502,10 @@ impl World { (groups, entries) }); - - client - .send_game_packet(&CCreativeContent { groups, entries }) - .await; + let creative_content = CCreativeContent { groups, entries }; + if let Ok(data) = client.serialize_packet(&creative_content) { + client.send_game_packet(data).await; + } let bedrock_recipes = BEDROCK_CRAFTING_DATA.get_or_init(|| { use pumpkin_data::item::{Item, JavaToBedrockItemMapping}; @@ -2556,12 +2679,13 @@ impl World { mapped_recipes }); - client - .send_game_packet(&pumpkin_protocol::bedrock::client::CCraftingData { - recipes: bedrock_recipes.clone(), - clean_recipes: false, - }) - .await; + let crafting_data = pumpkin_protocol::bedrock::client::CCraftingData { + recipes: bedrock_recipes.clone(), + clean_recipes: false, + }; + if let Ok(data) = client.serialize_packet(&crafting_data) { + client.send_game_packet(data).await; + } player .on_screen_handler_opened(player.player_screen_handler.clone()) @@ -2584,7 +2708,9 @@ impl World { }, tick: VarULong(0), }; - client.send_game_packet(&actor_data).await; + if let Ok(data) = client.serialize_packet(&actor_data) { + client.send_game_packet(data).await; + } player.send_abilities_update().await; { @@ -2594,7 +2720,7 @@ impl World { }; client - .enqueue_packet_internal(&CUpdateAttributes { + .enqueue_client_packet(&CUpdateAttributes { runtime_id: VarULong(runtime_id), attributes: vec![ Attribute { @@ -2808,7 +2934,7 @@ impl World { }], }; // Send PlayerList FIRST - client.send_game_packet(&ex_player_list).await; + client.send_packet(&ex_player_list).await; let ex_add_player = CAddPlayer { uuid: ex_profile.id, @@ -2847,7 +2973,7 @@ impl World { build_platform: BuildPlatform::Unknown, }; - client.send_game_packet(&ex_add_player).await; + client.send_packet(&ex_add_player).await; let ex_held_item = existing_player.inventory().held_item().await; @@ -2859,7 +2985,7 @@ impl World { 0, ); - client.send_game_packet(&ex_be_mob_equipment).await; + client.send_packet(&ex_be_mob_equipment).await; } player.has_played_before.store(true, Ordering::Relaxed); @@ -2908,7 +3034,7 @@ impl World { }; // Send the login packet for our new player client - .send_packet_now(&CLogin::new( + .send_packet(&CLogin::new( entity_id, base_config.hardcore, &dimensions, @@ -3002,7 +3128,7 @@ impl World { } } client - .send_packet_now(&pumpkin_protocol::java::client::play::CUpdateTagsPlay::new( + .send_packet(&pumpkin_protocol::java::client::play::CUpdateTagsPlay::new( &tags, )) .await; @@ -3049,11 +3175,11 @@ impl World { } } if client.version.load() >= JavaMinecraftVersion::V_1_20_2 { - client.send_packet_now(&CChunkBatchStart).await; + client.send_packet(&CChunkBatchStart).await; } - client.send_packet_now(&CChunkData(&chunk)).await; + client.send_packet(&CChunkData(&chunk)).await; if client.version.load() >= JavaMinecraftVersion::V_1_20_2 { - client.send_packet_now(&CChunkBatchEnd::new(1u16)).await; + client.send_packet(&CChunkBatchEnd::new(1u16)).await; } let velocity = player.living_entity.entity.velocity.load(); @@ -3199,7 +3325,7 @@ impl World { debug!("Sending player info to {}", player.gameprofile.name); client - .enqueue_packet(&CPlayerInfoUpdate::new(action_flags.bits(), &entries)) + .enqueue_client_packet(&CPlayerInfoUpdate::new(action_flags.bits(), &entries)) .await; // Send tab_list_names for existing players with custom names @@ -3211,7 +3337,7 @@ impl World { actions: &actions, }]; client - .enqueue_packet(&CPlayerInfoUpdate::new( + .enqueue_client_packet(&CPlayerInfoUpdate::new( PlayerInfoFlags::UPDATE_DISPLAY_NAME.bits(), &java_player, )) @@ -3440,7 +3566,7 @@ impl World { // END buf.put_u8(255); client - .enqueue_packet(&CSetEntityMetadata::new( + .enqueue_client_packet(&CSetEntityMetadata::new( existing_player.get_entity().entity_id.into(), buf.into(), )) @@ -3492,7 +3618,7 @@ impl World { // Start waiting for level chunks. Sets the "Loading Terrain" screen debug!("Sending waiting chunks to {}", player.gameprofile.name); client - .send_packet_now(&CGameEvent::new(GameEvent::StartWaitingChunks, 0.0)) + .send_packet(&CGameEvent::new(GameEvent::StartWaitingChunks, 0.0)) .await; self.worldborder.lock().await.init_client(client).await; @@ -3517,7 +3643,7 @@ impl World { }; client - .send_packet_now(&CPlayerSpawnPosition::new( + .send_packet(&CPlayerSpawnPosition::new( spawn_block_pos, yaw, pitch, @@ -3529,7 +3655,7 @@ impl World { let weather = self.weather.lock().await; if weather.raining { client - .enqueue_packet(&CGameEvent::new(GameEvent::BeginRaining, 0.0)) + .enqueue_client_packet(&CGameEvent::new(GameEvent::BeginRaining, 0.0)) .await; // Calculate rain and thunder levels directly from public fields @@ -3538,10 +3664,10 @@ impl World { drop(weather); client - .enqueue_packet(&CGameEvent::new(GameEvent::RainLevelChange, rain_level)) + .enqueue_client_packet(&CGameEvent::new(GameEvent::RainLevelChange, rain_level)) .await; client - .enqueue_packet(&CGameEvent::new( + .enqueue_client_packet(&CGameEvent::new( GameEvent::ThunderLevelChange, thunder_level, )) @@ -3566,13 +3692,15 @@ impl World { if let crate::net::ClientPlatform::Java(java_client) = player.client.as_ref() && server.advanced_config.recipe.send_recipes { - java_client - .send_packet_now(&CRecipeBookSettings::default_closed()) - .await; + let settings_packet = CRecipeBookSettings::default_closed(); + if let Ok(data) = java_client.serialize_packet(&settings_packet) { + java_client.send_packet_now(data).await; + } let dynamic_recipes = server.recipe_manager.get_dynamic_recipes().await; - java_client - .send_packet_now(&CRecipeBookAdd::new(true, &dynamic_recipes)) - .await; + let add_packet = CRecipeBookAdd::new(true, &dynamic_recipes); + if let Ok(data) = java_client.serialize_packet(&add_packet) { + java_client.send_packet_now(data).await; + } } let msg_comp = TextComponent::translate_cross( @@ -3640,8 +3768,7 @@ impl World { // TODO: World spawn (compass stuff) player - .client - .enqueue_packet(&CGameEvent::new(GameEvent::StartWaitingChunks, 0.0)) + .send_client_packet(&CGameEvent::new(GameEvent::StartWaitingChunks, 0.0)) .await; let entity = &player.get_entity(); @@ -3712,8 +3839,7 @@ impl World { continue; } player - .client - .enqueue_packet(&CExplosion::new( + .send_client_packet(&CExplosion::new( position, power, block_count as i32, @@ -3761,8 +3887,7 @@ impl World { } else { // No valid respawn point - send notification and use world spawn player - .client - .send_packet_now(&CGameEvent::new(GameEvent::NoRespawnBlockAvailable, 0.0)) + .send_client_packet(&CGameEvent::new(GameEvent::NoRespawnBlockAvailable, 0.0)) .await; // FIXME: This spawn position calculation is incorrect. Should use vanilla's @@ -3919,8 +4044,7 @@ impl World { // Send respawn packet with target dimension (using send_packet_now to ensure proper order) player - .client - .send_packet_now(&CRespawn::new( + .send_client_packet(&CRespawn::new( PlayerSpawnData::new( target_world.dimension.clone(), biome::hash_seed(target_world.level.seed.0), @@ -3950,7 +4074,6 @@ impl World { _ => 0, }; player - .client .send_packet_now_editioned( &CPlayerSpawnPosition::new( spawn_block_pos, @@ -3999,13 +4122,11 @@ impl World { .get_or_fetch_chunk(center_chunk, std::clone::Clone::clone) .await; if java_client.version.load() >= JavaMinecraftVersion::V_1_20_2 { - java_client.send_packet_now(&CChunkBatchStart).await; + java_client.send_packet(&CChunkBatchStart).await; } - java_client.send_packet_now(&CChunkData(&chunk)).await; + java_client.send_packet(&CChunkData(&chunk)).await; if java_client.version.load() >= JavaMinecraftVersion::V_1_20_2 { - java_client - .send_packet_now(&CChunkBatchEnd::new(1u16)) - .await; + java_client.send_packet(&CChunkBatchEnd::new(1u16)).await; } } @@ -4756,7 +4877,7 @@ impl World { if let Some(player) = self.get_player_by_uuid(from.entity_uuid) && let ClientPlatform::Bedrock(client) = player.client.as_ref() { - client.enqueue_packet(&be_packet).await; + client.enqueue_client_packet(&be_packet).await; } self.broadcast_to_chunk_except_editioned( @@ -5096,7 +5217,7 @@ impl World { match &cause { Some(player) => { if let ClientPlatform::Bedrock(client) = player.client.as_ref() { - client.enqueue_packet(&be_particles_packet).await; + client.enqueue_client_packet(&be_particles_packet).await; } self.broadcast_to_chunk_except_editioned( chunk_pos, @@ -6045,15 +6166,17 @@ impl World { packet: &P, ) { let players = self.players.load(); - for player in players.iter().filter(|player| { + let recipients = players.iter().filter_map(|player| { let center = player.get_entity().chunk_pos.load(); let view_distance = get_view_distance(player).get() as i32; - is_within_view_distance(chunk_pos, center, view_distance) - }) { - if let ClientPlatform::Bedrock(client) = player.client.as_ref() { - client.try_enqueue_packet(packet); + if is_within_view_distance(chunk_pos, center, view_distance) + && let ClientPlatform::Bedrock(client) = player.client.as_ref() + { + return Some(client); } - } + None + }); + Self::broadcast_bedrock_grouped(packet, recipients); } pub fn broadcast_to_chunk_editioned_sync( @@ -6064,6 +6187,7 @@ impl World { ) { let players = self.players.load(); let mut java_recipients = Vec::new(); + let mut bedrock_recipients = Vec::new(); let recipients = players.iter().filter(|p| { let center = p.get_entity().chunk_pos.load(); @@ -6074,13 +6198,14 @@ impl World { for p in recipients { match p.client.as_ref() { ClientPlatform::Java(_) => java_recipients.push(p), - ClientPlatform::Bedrock(be_client) => be_client.try_enqueue_packet(be_packet), + ClientPlatform::Bedrock(be_client) => bedrock_recipients.push(be_client), } } let recipients_by_version = Self::collect_java_recipients_by_version(java_recipients.into_iter()); Self::broadcast_java_grouped(je_packet, recipients_by_version); + Self::broadcast_bedrock_grouped(be_packet, bedrock_recipients.into_iter()); } /// Broadcasts a packet to chunk watchers, excluding specific players. @@ -6130,17 +6255,14 @@ impl World { for p in recipients { match p.client.as_ref() { ClientPlatform::Java(_) => java_recipients.push(p), - ClientPlatform::Bedrock(be_client) => bedrock_recipients.push(be_client.clone()), + ClientPlatform::Bedrock(be_client) => bedrock_recipients.push(be_client), } } let je_recipients_by_version = Self::collect_java_recipients_by_version(java_recipients.into_iter()); Self::broadcast_java_grouped(je_packet, je_recipients_by_version); - - for recipient in bedrock_recipients { - recipient.enqueue_packet(be_packet).await; - } + Self::broadcast_bedrock_grouped_async(be_packet, bedrock_recipients.into_iter()).await; } pub async fn emit_game_event(&self, event_key: impl Into, position: Vector3) { diff --git a/crates/pumpkin/src/world/portal/mod.rs b/crates/pumpkin/src/world/portal/mod.rs index 8b523c481..d314f63d7 100644 --- a/crates/pumpkin/src/world/portal/mod.rs +++ b/crates/pumpkin/src/world/portal/mod.rs @@ -137,7 +137,7 @@ impl PortalType { match player.client.as_ref() { crate::net::ClientPlatform::Java(client) => { client - .enqueue_packet(&pumpkin_protocol::java::client::play::CGameEvent::new( + .enqueue_client_packet(&pumpkin_protocol::java::client::play::CGameEvent::new( pumpkin_protocol::java::client::play::GameEvent::WinGame, 1.0, )) @@ -145,7 +145,7 @@ impl PortalType { } crate::net::ClientPlatform::Bedrock(client) => { client - .send_game_packet( + .send_packet( &pumpkin_protocol::bedrock::client::CShowCredits::new( pumpkin_protocol::codec::var_ulong::VarULong( caller.get_entity().entity_id as u64,