chore: add packet rate limiter

This commit is contained in:
Alexander Medvedev 2026-08-19 11:53:53 +02:00
parent 5c212b38e0
commit 399bf14150
70 changed files with 1678 additions and 1096 deletions

View File

@ -36,6 +36,7 @@ pub use networking::bedrock::BedrockConfig;
pub use networking::compression::CompressionConfig; pub use networking::compression::CompressionConfig;
pub use networking::java::JavaConfig; pub use networking::java::JavaConfig;
pub use networking::lan_broadcast::LANBroadcastConfig; pub use networking::lan_broadcast::LANBroadcastConfig;
pub use networking::packet_limiter::PacketLimiterConfig;
pub use networking::rcon::RCONConfig; pub use networking::rcon::RCONConfig;
pub use plugins::{PluginOverride, PluginsConfig}; pub use plugins::{PluginOverride, PluginsConfig};
pub use pvp::PVPConfig; pub use pvp::PVPConfig;
@ -93,6 +94,10 @@ impl LoadConfiguration for PumpkinConfig {
}; };
// Validate Java // Validate Java
assert!(
self.advanced.networking.java.keep_alive_time > 0,
"Java Keep alive time must be greater than 0"
);
assert!( assert!(
self.advanced.networking.java.view_distance >= min_vd, self.advanced.networking.java.view_distance >= min_vd,
"Java View distance must be at least 2" "Java View distance must be at least 2"

View File

@ -1,4 +1,4 @@
use crate::CompressionConfig; use crate::{CompressionConfig, PacketLimiterConfig};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::net::{IpAddr, SocketAddr}; use std::net::{IpAddr, SocketAddr};
use std::num::NonZero; use std::num::NonZero;
@ -84,6 +84,8 @@ pub struct BedrockConfig {
pub nethernet: NetherNetConfig, pub nethernet: NetherNetConfig,
/// Whether Bedrock client chunk blob caching is enabled. /// Whether Bedrock client chunk blob caching is enabled.
pub chunk_caching: bool, pub chunk_caching: bool,
/// Packet rate limiting settings.
pub packet_limiter: PacketLimiterConfig,
} }
impl Default for BedrockConfig { impl Default for BedrockConfig {
@ -101,6 +103,7 @@ impl Default for BedrockConfig {
authentication: BedrockAuthenticationConfig::default(), authentication: BedrockAuthenticationConfig::default(),
nethernet: NetherNetConfig::default(), nethernet: NetherNetConfig::default(),
chunk_caching: true, chunk_caching: true,
packet_limiter: PacketLimiterConfig::default(),
} }
} }
} }

View File

@ -1,4 +1,4 @@
use crate::{AuthenticationConfig, CompressionConfig}; use crate::{AuthenticationConfig, CompressionConfig, PacketLimiterConfig};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::net::SocketAddr; use std::net::SocketAddr;
use std::num::NonZero; use std::num::NonZero;
@ -21,12 +21,21 @@ pub struct JavaConfig {
pub view_distance: NonZero<u8>, pub view_distance: NonZero<u8>,
/// The maximum simulated view distance. /// The maximum simulated view distance.
pub simulation_distance: NonZero<u8>, pub simulation_distance: NonZero<u8>,
/// 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. /// Java Edition packet compression settings.
pub compression: CompressionConfig, pub compression: CompressionConfig,
/// Message of the Day; the server's description displayed on the status screen. /// Message of the Day; the server's description displayed on the status screen.
pub motd: String, pub motd: String,
/// Authentication settings for client connections. /// Authentication settings for client connections.
pub authentication: AuthenticationConfig, pub authentication: AuthenticationConfig,
/// Packet rate limiting settings.
pub packet_limiter: PacketLimiterConfig,
} }
impl Default for JavaConfig { impl Default for JavaConfig {
@ -44,9 +53,49 @@ impl Default for JavaConfig {
max_players: 1000, max_players: 1000,
view_distance, view_distance,
simulation_distance, simulation_distance,
keep_alive_time: 15,
compression: CompressionConfig::default(), compression: CompressionConfig::default(),
motd: "A blazingly fast Pumpkin server!".to_string(), motd: "A blazingly fast Pumpkin server!".to_string(),
authentication: AuthenticationConfig::default(), 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);
}
}

View File

@ -24,6 +24,10 @@ pub mod query;
/// RCON remote console configuration. /// RCON remote console configuration.
pub mod rcon; pub mod rcon;
/// Packet limiter configuration.
pub mod packet_limiter;
pub use packet_limiter::PacketLimiterConfig;
/// Configuration for server networking features. /// Configuration for server networking features.
/// ///
/// Covers authentication, query, RCON, proxying, packet compression, /// Covers authentication, query, RCON, proxying, packet compression,

View File

@ -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");
}
}

View File

@ -190,8 +190,8 @@ pub mod serverbound {
v1_21_7: 0i32, v1_21_7: 0i32,
v1_21_9: 0i32, v1_21_9: 0i32,
v1_21_11: 0i32, v1_21_11: 0i32,
v26_1: -1i32, v26_1: 0i32,
v26_2: -1i32, v26_2: 0i32,
}; };
pub const LOGIN_COOKIE_RESPONSE: super::PacketId = super::PacketId { pub const LOGIN_COOKIE_RESPONSE: super::PacketId = super::PacketId {
v1_20_5: 4i32, v1_20_5: 4i32,

View File

@ -86,6 +86,44 @@ impl BedrockBatchEncoder {
Ok(()) Ok(())
} }
pub fn write_packet<P: crate::BClientPacket + ?Sized>(
&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<P: crate::BClientPacket + ?Sized>(
&self,
packet: &P,
) -> Result<bytes::Bytes, Error> {
let mut buf = Vec::new();
self.write_packet(packet, &mut buf)?;
Ok(buf.into())
}
}
pub fn write_packet<P: crate::BClientPacket + ?Sized>(
packet: &P,
writer: impl Write,
) -> Result<(), Error> {
BedrockBatchEncoder::new().write_packet(packet, writer)
}
pub fn serialize_packet<P: crate::BClientPacket + ?Sized>(
packet: &P,
) -> Result<bytes::Bytes, Error> {
BedrockBatchEncoder::new().serialize_packet(packet)
} }
#[cfg(test)] #[cfg(test)]

View File

@ -27,3 +27,15 @@ impl ClientPacket for CConfigPing {
Ok(()) Ok(())
} }
} }
impl<'a> crate::ServerPacket<'a> for CConfigPing {
fn read(
read: &mut &'a [u8],
_version: &JavaMinecraftVersion,
) -> Result<Self, crate::ser::ReadingError> {
use crate::ser::NetworkReadExt;
Ok(Self {
id: read.get_i32_be()?,
})
}
}

View File

@ -91,16 +91,50 @@ impl ClientPacket for CPlayerPosition {
impl<'a> ServerPacket<'a> for CPlayerPosition { impl<'a> ServerPacket<'a> for CPlayerPosition {
fn read( fn read(
read: &mut &'a [u8], read: &mut &'a [u8],
_version: &JavaMinecraftVersion, version: &JavaMinecraftVersion,
) -> Result<Self, crate::ser::ReadingError> { ) -> Result<Self, crate::ser::ReadingError> {
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 { Ok(Self {
teleport_id: read.get_var_int()?, teleport_id,
// TODO position: Vector3::new(x, y, z),
position: Vector3::new(0.0, 0.0, 0.0), 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), delta: Vector3::new(0.0, 0.0, 0.0),
yaw: 0.0, yaw,
pitch: 0.0, pitch,
relatives: Vec::new(), relatives: PositionFlag::from_bitfield(relatives_bits),
}) })
} }
} }
}

View File

@ -1,12 +1,14 @@
use aes::cipher::KeyIvInit; use aes::cipher::KeyIvInit;
use bytes::Bytes; use bytes::Bytes;
use flate2::{Compress, Compression, FlushCompress, Status}; use flate2::{Compress, Compression, FlushCompress, Status};
use pumpkin_util::version::JavaMinecraftVersion;
use thiserror::Error; use thiserror::Error;
use tokio::io::{AsyncWrite, AsyncWriteExt}; use tokio::io::{AsyncWrite, AsyncWriteExt};
use crate::{ use crate::{
Aes128Cfb8Enc, CompressionLevel, CompressionThreshold, MAX_PACKET_DATA_SIZE, MAX_PACKET_SIZE, Aes128Cfb8Enc, ClientPacket, CompressionLevel, CompressionThreshold, MAX_PACKET_DATA_SIZE,
PacketEncodeError, StreamEncryptor, VarInt, MAX_PACKET_SIZE, PacketEncodeError, StreamEncryptor, VarInt, WritingError,
ser::NetworkWriteExt,
}; };
// raw -> compress -> encrypt // raw -> compress -> encrypt
@ -318,6 +320,28 @@ impl<W: AsyncWrite + Unpin> TCPNetworkEncoder<W> {
} }
} }
pub fn write_packet<P: ClientPacket + ?Sized>(
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<P: ClientPacket + ?Sized>(
packet: &P,
version: &JavaMinecraftVersion,
) -> Result<Bytes, WritingError> {
let mut packet_buf = Vec::new();
write_packet(packet, version, &mut packet_buf)?;
Ok(packet_buf.into())
}
#[derive(Error, Debug)] #[derive(Error, Debug)]
#[error("Invalid compression Level")] #[error("Invalid compression Level")]
pub struct CompressionLevelError; pub struct CompressionLevelError;

View File

@ -255,6 +255,18 @@ pub trait ClientPacket: MultiVersionJavaPacket {
write: impl Write, write: impl Write,
version: &JavaMinecraftVersion, version: &JavaMinecraftVersion,
) -> Result<(), WritingError>; ) -> 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<Bytes, WritingError> {
crate::java::packet_encoder::serialize_packet(self, version)
}
} }
pub trait ServerPacket<'a>: MultiVersionJavaPacket + Sized { pub trait ServerPacket<'a>: MultiVersionJavaPacket + Sized {
@ -263,6 +275,10 @@ pub trait ServerPacket<'a>: MultiVersionJavaPacket + Sized {
pub trait BClientPacket: Packet { pub trait BClientPacket: Packet {
fn write_packet(&self, writer: impl Write) -> Result<(), Error>; fn write_packet(&self, writer: impl Write) -> Result<(), Error>;
fn serialize_packet(&self) -> Result<Bytes, Error> {
crate::bedrock::packet_encoder::serialize_packet(self)
}
} }
pub trait BServerPacket: Packet + Sized { pub trait BServerPacket: Packet + Sized {
@ -447,6 +463,24 @@ impl PositionFlag {
pub fn get_bitfield(flags: &[Self]) -> i32 { pub fn get_bitfield(flags: &[Self]) -> i32 {
flags.iter().fold(0, |acc, flag| acc | flag.get_mask()) flags.iter().fold(0, |acc, flag| acc | flag.get_mask())
} }
#[must_use]
pub fn from_bitfield(bits: i32) -> Vec<Self> {
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 { pub enum Label {

View File

@ -1282,6 +1282,12 @@ impl GenerationSchedule {
}); });
if node.stage > effective_target { 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.waiting_for_chunks.remove(&task.1);
self.drop_node(task.1); self.drop_node(task.1);
continue; continue;

View File

@ -416,3 +416,47 @@ fn dag_prune_edge_chain_removes_dead_target_edges() {
assert!(head.is_null()); assert!(head.is_null());
assert_eq!(graph.edges.len(), 0); 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"
);
}

View File

@ -208,7 +208,7 @@ pub async fn send_c_commands_packet(
} }
let packet = CCommands::new(proto_nodes.into(), VarInt(root_node_index as i32)); 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 { 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() { 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;
} }
} }

View File

@ -28,7 +28,7 @@ impl CommandExecutor for DialogClearExecutor {
let count = targets.len(); let count = targets.len();
let packet = CPlayClearDialog::new(); let packet = CPlayClearDialog::new();
for player in &targets { for player in &targets {
player.client.send_packet_now(&packet).await; player.send_client_packet(&packet).await;
} }
let msg = if count == 1 { let msg = if count == 1 {
@ -65,7 +65,7 @@ impl CommandExecutor for DialogShowExecutor {
let packet = CPlayShowDialog::new(IdOr::Value(dialog_nbt)); let packet = CPlayShowDialog::new(IdOr::Value(dialog_nbt));
for player in &targets { for player in &targets {
player.client.send_packet_now(&packet).await; player.send_client_packet(&packet).await;
} }
let msg = if count == 1 { let msg = if count == 1 {

View File

@ -108,13 +108,10 @@ impl CommandExecutor for RecipeGiveExecutor {
} }
let recipe_count = matching_recipes.len(); let recipe_count = matching_recipes.len();
let packet = CRecipeBookAdd::new(false, &matching_recipes);
for player in &targets { for player in &targets {
if let crate::net::ClientPlatform::Java(java_client) = player.client.as_ref() { player.send_client_packet(&packet).await;
java_client
.send_packet_now(&CRecipeBookAdd::new(false, &matching_recipes))
.await;
}
} }
let recipe_count_str = recipe_count.to_string(); let recipe_count_str = recipe_count.to_string();
@ -193,12 +190,9 @@ impl CommandExecutor for RecipeTakeExecutor {
all_recipes.len() - remaining_recipes.len() all_recipes.len() - remaining_recipes.len()
}; };
let packet = CRecipeBookAdd::new(true, &remaining_recipes);
for player in &targets { for player in &targets {
if let crate::net::ClientPlatform::Java(java_client) = player.client.as_ref() { player.send_client_packet(&packet).await;
java_client
.send_packet_now(&CRecipeBookAdd::new(true, &remaining_recipes))
.await;
}
} }
let taken_count_str = taken_count.to_string(); let taken_count_str = taken_count.to_string();

View File

@ -45,8 +45,7 @@ impl CommandExecutor for StopSpectateExecutor {
player.camera_target_id.store(None); player.camera_target_id.store(None);
player player
.client .send_client_packet(&CSetCamera::new(player.entity_id().into()))
.send_packet_now(&CSetCamera::new(player.entity_id().into()))
.await; .await;
sender sender
@ -110,8 +109,7 @@ impl CommandExecutor for SpectateTargetSelfExecutor {
let target_id = target_entity.entity_id; let target_id = target_entity.entity_id;
player.camera_target_id.store(Some(target_id)); player.camera_target_id.store(Some(target_id));
player player
.client .send_client_packet(&CSetCamera::new(target_id.into()))
.send_packet_now(&CSetCamera::new(target_id.into()))
.await; .await;
let pos = target_entity.pos.load(); let pos = target_entity.pos.load();
@ -189,8 +187,7 @@ impl CommandExecutor for SpectateTargetOtherExecutor {
let target_id = target_entity.entity_id; let target_id = target_entity.entity_id;
player.camera_target_id.store(Some(target_id)); player.camera_target_id.store(Some(target_id));
player player
.client .send_client_packet(&CSetCamera::new(target_id.into()))
.send_packet_now(&CSetCamera::new(target_id.into()))
.await; .await;
let pos = target_entity.pos.load(); let pos = target_entity.pos.load();

View File

@ -44,7 +44,7 @@ impl CommandExecutor for ClearOrResetExecutor {
let reset = self.0; let reset = self.0;
for target in targets { for target in targets {
target.client.enqueue_packet(&CClearTitle::new(reset)).await; target.send_client_packet(&CClearTitle::new(reset)).await;
} }
sender sender
.send_message(if targets.len() == 1 { .send_message(if targets.len() == 1 {

View File

@ -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::tree::builder::{argument, argument_default_name, require};
use crate::command::{CommandExecutor, CommandSender, args::ConsumedArgs, tree::CommandTree}; use crate::command::{CommandExecutor, CommandSender, args::ConsumedArgs, tree::CommandTree};
use crate::entity::EntityBase; use crate::entity::EntityBase;
use crate::net::ClientPlatform;
const NAMES: [&str; 1] = ["transfer"]; const NAMES: [&str; 1] = ["transfer"];
@ -58,22 +57,12 @@ impl CommandExecutor for TargetSelfExecutor {
let name = &player.gameprofile.name; let name = &player.gameprofile.name;
info!("[{name}: Transferring {name} to {hostname}:{port}]"); info!("[{name}: Transferring {name} to {hostname}:{port}]");
match player.client.as_ref() { player
ClientPlatform::Java(client) => { .enqueue_packet_editioned(
client &JavaCTransfer::new(hostname, VarInt(port)),
.enqueue_packet(&JavaCTransfer::new(hostname, VarInt(port))) &BedrockCTransfer::new(hostname.to_string(), port as u16, false),
)
.await; .await;
}
ClientPlatform::Bedrock(client) => {
client
.send_game_packet(&BedrockCTransfer::new(
hostname.to_string(),
port as u16,
false,
))
.await;
}
}
Ok(1) Ok(1)
} else { } else {
@ -121,22 +110,11 @@ impl CommandExecutor for TargetPlayerExecutor {
} }
for p in players { for p in players {
match p.client.as_ref() { p.enqueue_packet_editioned(
ClientPlatform::Java(client) => { &JavaCTransfer::new(hostname, VarInt(port)),
client &BedrockCTransfer::new(hostname.to_string(), port as u16, false),
.enqueue_packet(&JavaCTransfer::new(hostname, VarInt(port))) )
.await; .await;
}
ClientPlatform::Bedrock(client) => {
client
.send_game_packet(&BedrockCTransfer::new(
hostname.to_string(),
port as u16,
false,
))
.await;
}
}
info!( info!(
"[{sender}: Transferring {} to {hostname}:{port}]", "[{sender}: Transferring {} to {hostname}:{port}]",

View File

@ -90,7 +90,7 @@ impl CommandExecutor for ColorExecutor {
); );
if let Some(player) = sender.as_player() { if let Some(player) = sender.as_player() {
player.client.enqueue_packet(&packet).await; player.send_client_packet(&packet).await;
} }
match self.0 { match self.0 {
@ -173,7 +173,7 @@ impl CommandExecutor for StyleExecutor {
); );
if let Some(player) = sender.as_player() { if let Some(player) = sender.as_player() {
player.client.enqueue_packet(&packet).await; player.send_client_packet(&packet).await;
} }
sender sender

View File

@ -348,9 +348,10 @@ impl EntityBase for ItemFrameEntity {
client: &'a crate::net::java::JavaClient, client: &'a crate::net::java::JavaClient,
) -> EntityBaseFuture<'a, ()> { ) -> EntityBaseFuture<'a, ()> {
Box::pin(async move { Box::pin(async move {
client let spawn_packet = self.entity.create_spawn_packet();
.enqueue_packet(&self.entity.create_spawn_packet()) if let Ok(data) = client.serialize_packet(&spawn_packet) {
.await; client.enqueue_packet(data).await;
}
let ver = client.version.load(); let ver = client.version.load();
if ver >= CURRENT_MC_VERSION { if ver >= CURRENT_MC_VERSION {
@ -370,12 +371,11 @@ impl EntityBase for ItemFrameEntity {
&& meta_rot.write(&mut data, &ver).is_ok() && meta_rot.write(&mut data, &ver).is_ok()
{ {
data.push(255); data.push(255);
client let meta_packet =
.enqueue_packet(&CSetEntityMetadata::new( CSetEntityMetadata::new(self.entity.entity_id.into(), data.into());
self.entity.entity_id.into(), if let Ok(meta_data) = client.serialize_packet(&meta_packet) {
data.into(), client.enqueue_packet(meta_data).await;
)) }
.await;
} }
} }
}) })

View File

@ -656,7 +656,9 @@ impl EntityBase for ItemEntity {
metadata: entity.bedrock_metadata(), metadata: entity.bedrock_metadata(),
from_fishing: false, 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, client: &'a crate::net::java::JavaClient,
) -> EntityBaseFuture<'a, ()> { ) -> EntityBaseFuture<'a, ()> {
Box::pin(async move { Box::pin(async move {
client let spawn_packet = self.entity.create_spawn_packet();
.enqueue_packet(&self.entity.create_spawn_packet()) if let Ok(data) = client.serialize_packet(&spawn_packet) {
.await; client.enqueue_packet(data).await;
}
if client.version.load() >= CURRENT_MC_VERSION { if client.version.load() >= CURRENT_MC_VERSION {
let metadata = Metadata::new( let metadata = Metadata::new(
@ -677,12 +680,11 @@ impl EntityBase for ItemEntity {
let mut data = Vec::new(); let mut data = Vec::new();
if metadata.write(&mut data, &client.version.load()).is_ok() { if metadata.write(&mut data, &client.version.load()).is_ok() {
data.push(255); data.push(255);
client let meta_packet =
.enqueue_packet(&CSetEntityMetadata::new( CSetEntityMetadata::new(self.entity.entity_id.into(), data.into());
self.entity.entity_id.into(), if let Ok(meta_data) = client.serialize_packet(&meta_packet) {
data.into(), client.enqueue_packet(meta_data).await;
)) }
.await;
} }
} }
}) })

View File

@ -333,24 +333,26 @@ pub trait EntityBase: Send + Sync + NBTStorage + std::any::Any {
}, },
Vec::new(), 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, ()> { fn send_java_spawn_packet<'a>(&'a self, client: &'a JavaClient) -> EntityBaseFuture<'a, ()> {
Box::pin(async move { Box::pin(async move {
client let spawn_packet = self.get_entity().create_spawn_packet();
.enqueue_packet(&self.get_entity().create_spawn_packet()) if let Ok(data) = client.serialize_packet(&spawn_packet) {
.await; client.enqueue_packet(data).await;
}
if let Some(mob) = self.get_mob() if let Some(mob) = self.get_mob()
&& let Some(metadata) = mob.mob_java_spawn_metadata(client.version.load()).await && let Some(metadata) = mob.mob_java_spawn_metadata(client.version.load()).await
{ {
client let meta_packet =
.enqueue_packet(&CSetEntityMetadata::new( CSetEntityMetadata::new(self.get_entity().entity_id.into(), metadata);
self.get_entity().entity_id.into(), if let Ok(meta_data) = client.serialize_packet(&meta_packet) {
metadata, client.enqueue_packet(meta_data).await;
)) }
.await;
} }
}) })
} }
@ -1862,15 +1864,7 @@ impl Entity {
0, 0,
); );
let world = self.world.load(); let world = self.world.load();
for player in world.players.load().iter() { world.broadcast_to_chunk_bedrock(chunk_pos, &packet);
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);
}
}
} }
pub fn update_last_pos(&self) -> Vector3<f64> { pub fn update_last_pos(&self) -> Vector3<f64> {
@ -2684,10 +2678,11 @@ impl Entity {
)); ));
} }
#[expect(clippy::unused_async)]
pub async fn set_sneaking(&self, sneaking: bool) { pub async fn set_sneaking(&self, sneaking: bool) {
//assert!(self.sneaking.load(Relaxed) != sneaking); //assert!(self.sneaking.load(Relaxed) != sneaking);
self.sneaking.store(sneaking, Relaxed); self.sneaking.store(sneaking, Relaxed);
self.set_flag(Flag::Sneaking, sneaking).await; self.set_flag(Flag::Sneaking, sneaking);
} }
pub fn is_sneaking(&self) -> bool { pub fn is_sneaking(&self) -> bool {
self.sneaking.load(Ordering::Relaxed) self.sneaking.load(Ordering::Relaxed)
@ -2707,31 +2702,34 @@ impl Entity {
return; return;
} }
self.swimming.store(event.is_swimming, Relaxed); 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. /// Sets whether the entity is invisible and sends updated metadata.
#[expect(clippy::unused_async)]
pub async fn set_invisible(&self, invisible: bool) { pub async fn set_invisible(&self, invisible: bool) {
if self.invisible.load(Ordering::Relaxed) != invisible { if self.invisible.load(Ordering::Relaxed) != invisible {
self.invisible.store(invisible, Relaxed); 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. /// Sets whether the entity is glowing and sends updated metadata.
#[expect(clippy::unused_async)]
pub async fn set_glowing(&self, glowing: bool) { pub async fn set_glowing(&self, glowing: bool) {
if self.glowing.load(Ordering::Relaxed) != glowing { if self.glowing.load(Ordering::Relaxed) != glowing {
self.glowing.store(glowing, Ordering::Relaxed); 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. /// 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) { pub async fn set_on_fire(&self, on_fire: bool) {
if self.has_visual_fire.load(Ordering::Relaxed) != on_fire { if self.has_visual_fire.load(Ordering::Relaxed) != on_fire {
self.has_visual_fire.store(on_fire, Ordering::Relaxed); 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) { pub async fn set_sprinting(&self, sprinting: bool) {
//assert!(self.sprinting.load(Relaxed) != sprinting); //assert!(self.sprinting.load(Relaxed) != sprinting);
self.sprinting.store(sprinting, Relaxed); self.sprinting.store(sprinting, Relaxed);
self.set_flag(Flag::Sprinting, sprinting).await; self.set_flag(Flag::Sprinting, sprinting);
} }
pub fn is_sprinting(&self) -> bool { pub fn is_sprinting(&self) -> bool {
@ -2853,16 +2852,17 @@ impl Entity {
!self.on_ground.load(Relaxed) !self.on_ground.load(Relaxed)
} }
#[expect(clippy::unused_async)]
pub async fn set_fall_flying(&self, fall_flying: bool) { pub async fn set_fall_flying(&self, fall_flying: bool) {
assert_ne!(self.fall_flying.load(Relaxed), fall_flying); assert_ne!(self.fall_flying.load(Relaxed), fall_flying);
self.fall_flying.store(fall_flying, Relaxed); 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 { pub fn is_fall_flying(&self) -> bool {
self.fall_flying.load(Ordering::Relaxed) 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 index = flag as u8;
let mask = (1i8).wrapping_shl(index as u32); let mask = (1i8).wrapping_shl(index as u32);
let new_je_flags = if value { let new_je_flags = if value {
@ -2904,13 +2904,6 @@ impl Entity {
let world = self.world.load(); let world = self.world.load();
let chunk_pos = self.chunk_pos.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()); let mut metadata = EntityMetadata(std::collections::HashMap::new());
metadata.set( metadata.set(
entity_data_key::FLAGS, entity_data_key::FLAGS,
@ -2920,8 +2913,7 @@ impl Entity {
entity_data_key::FLAGS_TWO, entity_data_key::FLAGS_TWO,
MetadataValue::Long(self.bedrock_flags_two.load(Ordering::Relaxed)), MetadataValue::Long(self.bedrock_flags_two.load(Ordering::Relaxed)),
); );
client let packet = CSetActorData {
.enqueue_packet(&CSetActorData {
actor_runtime_id: VarULong(self.entity_id as u64), actor_runtime_id: VarULong(self.entity_id as u64),
metadata, metadata,
synced_properties: PropertySyncData { synced_properties: PropertySyncData {
@ -2929,11 +2921,8 @@ impl Entity {
float_properties: std::collections::HashMap::new(), float_properties: std::collections::HashMap::new(),
}, },
tick: VarULong(0), tick: VarULong(0),
}) };
.await; world.broadcast_to_chunk_bedrock(chunk_pos, &packet);
}
}
}
} }
} }
@ -2951,39 +2940,45 @@ impl Entity {
) { ) {
let world = self.world.load(); let world = self.world.load();
let chunk_pos = self.chunk_pos.load(); let chunk_pos = self.chunk_pos.load();
let players = world.players.load();
for player in world.players.load().iter() { let mut java_recipients = Vec::new();
let mut bedrock_recipients = Vec::new();
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() { match player.client.as_ref() {
ClientPlatform::Java(client) => { ClientPlatform::Java(_) => java_recipients.push(player),
let version = client.version.load(); ClientPlatform::Bedrock(client) => bedrock_recipients.push(client),
}
}
}
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 { if version < CURRENT_MC_VERSION {
continue; 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;
if is_within_view_distance(chunk_pos, center, view_distance) {
let mut buf = Vec::new(); let mut buf = Vec::new();
for m in meta { for m in meta {
let _ = m.write(&mut buf, &version); let _ = m.write(&mut buf, &version);
} }
buf.put_u8(255); buf.put_u8(255);
player.client.try_enqueue_packet(&CSetEntityMetadata::new( let packet = CSetEntityMetadata::new(self.entity_id.into(), buf.into());
self.entity_id.into(), if let Ok(packet_data) = JavaClient::serialize_packet_for_version(&packet, version) {
buf.into(), for recipient in recipients {
)); recipient.try_enqueue_packet(packet_data.clone());
}
} }
} }
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) { if let Some(bedrock_meta) = bedrock_meta {
client.try_enqueue_packet(&CSetActorData { let packet = CSetActorData {
actor_runtime_id: VarULong(self.entity_id as u64), actor_runtime_id: VarULong(self.entity_id as u64),
metadata: EntityMetadata(bedrock_meta.0.clone()), metadata: EntityMetadata(bedrock_meta.0.clone()),
synced_properties: PropertySyncData { synced_properties: PropertySyncData {
@ -2991,9 +2986,12 @@ impl Entity {
float_properties: std::collections::HashMap::new(), float_properties: std::collections::HashMap::new(),
}, },
tick: VarULong(0), 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 world = self.world.load();
let passengers_packet = CSetPassengers::new(VarInt(self.entity_id), &passenger_ids); let passengers_packet = CSetPassengers::new(VarInt(self.entity_id), &passenger_ids);
if let Some(player) = passenger.get_player() { 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( world.broadcast_to_chunk_except(
chunk_pos, chunk_pos,
&[player.get_entity().entity_uuid], &[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(player) = passenger.get_player() {
if let Some(id) = teleport_id { if let Some(id) = teleport_id {
player.get_entity().set_pos(dismount_pos); player.get_entity().set_pos(dismount_pos);
// Update awaiting_teleport with the real dismount position // Update awaiting_teleport with the real dismount position
*player.awaiting_teleport.lock().await = Some((id.into(), dismount_pos)); *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. // the same packet queue as CSetPassengers, preserving send order.
// Vanilla uses DELTA | ROT flags: position absolute, delta/rotation relative. // Vanilla uses DELTA | ROT flags: position absolute, delta/rotation relative.
// With rotation relative and yaw/pitch=0, the client preserves its current look. // With rotation relative and yaw/pitch=0, the client preserves its current look.
player player
.client .send_client_packet(&CPlayerPosition::new(
.enqueue_packet(&CPlayerPosition::new(
id.into(), id.into(),
dismount_pos, dismount_pos,
Vector3::new(0.0, 0.0, 0.0), Vector3::new(0.0, 0.0, 0.0),

View File

@ -62,16 +62,16 @@ pub struct JavaPlayer<'a>(pub &'a Player);
impl JavaPlayer<'_> { impl JavaPlayer<'_> {
pub async fn send_packet<C: pumpkin_protocol::ClientPacket + Sync>(&self, packet: &C) { pub async fn send_packet<C: pumpkin_protocol::ClientPacket + Sync>(&self, packet: &C) {
if let ClientPlatform::Java(client) = self.0.client.as_ref() { if let ClientPlatform::Java(client) = self.0.client.as_ref()
client.enqueue_packet(packet).await; && let Ok(data) = client.serialize_packet(packet)
{
client.enqueue_packet(data).await;
} }
} }
pub async fn send_custom_payload(&self, channel: &str, data: &[u8]) { pub async fn send_custom_payload(&self, channel: &str, data: &[u8]) {
if let ClientPlatform::Java(java) = self.0.client.as_ref() { let packet = CCustomPayload::new(channel, data);
java.enqueue_packet(&CCustomPayload::new(channel, data)) self.send_packet(&packet).await;
.await;
}
} }
pub async fn send_stats(&self) { pub async fn send_stats(&self) {
@ -102,8 +102,10 @@ pub struct BedrockPlayer<'a>(pub &'a Player);
impl BedrockPlayer<'_> { impl BedrockPlayer<'_> {
pub async fn send_packet<P: pumpkin_protocol::BClientPacket + Sync>(&self, packet: &P) { pub async fn send_packet<P: pumpkin_protocol::BClientPacket + Sync>(&self, packet: &P) {
if let ClientPlatform::Bedrock(client) = self.0.client.as_ref() { if let ClientPlatform::Bedrock(client) = self.0.client.as_ref()
client.send_game_packet(packet).await; && 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) { pub async fn set_tab_list_header_footer(&self, header: TextComponent, footer: TextComponent) {
*self.tab_list_header.lock().await = header.clone(); *self.tab_list_header.lock().await = header.clone();
*self.tab_list_footer.lock().await = footer.clone(); *self.tab_list_footer.lock().await = footer.clone();
self.client self.send_client_packet(&CTabList::new(&header, &footer))
.enqueue_packet(&CTabList::new(&header, &footer))
.await; .await;
} }
@ -1084,8 +1085,7 @@ impl Player {
duration, duration,
}, },
); );
self.client self.send_client_packet(&CItemCooldown::new(group, VarInt(duration)))
.send_packet_now(&CItemCooldown::new(group, VarInt(duration)))
.await; .await;
} }
@ -2227,15 +2227,8 @@ impl Player {
} }
pub async fn send_title_animation(&self, fade_in: i32, stay: i32, fade_out: i32) { pub async fn send_title_animation(&self, fade_in: i32, stay: i32, fade_out: i32) {
match self.client.as_ref() { self.enqueue_packet_editioned(
ClientPlatform::Java(client) => { &CTitleAnimation::new(fade_in, stay, fade_out),
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( &pumpkin_protocol::bedrock::client::set_title::CSetTitle::new(
5, 5,
String::new(), String::new(),
@ -2246,8 +2239,6 @@ impl Player {
) )
.await; .await;
} }
}
}
pub fn spawn_particle( pub fn spawn_particle(
&self, &self,
@ -2257,7 +2248,7 @@ impl Player {
particle_count: i32, particle_count: i32,
particle: Particle, particle: Particle,
) { ) {
self.client.try_enqueue_packet(&CParticle::new( let packet = CParticle::new(
false, false,
false, false,
position, position,
@ -2266,7 +2257,12 @@ impl Player {
particle_count, particle_count,
VarInt(particle as i32), 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( pub async fn play_sound(
@ -2278,16 +2274,8 @@ impl Player {
pitch: f32, pitch: f32,
seed: f64, seed: f64,
) { ) {
self.client let packet = CSoundEffect::new(IdOr::Id(sound_id), category, position, volume, pitch, seed);
.enqueue_packet(&CSoundEffect::new( self.send_client_packet(&packet).await;
IdOr::Id(sound_id),
category,
position,
volume,
pitch,
seed,
))
.await;
} }
pub async fn play_sound_event( pub async fn play_sound_event(
@ -2299,16 +2287,8 @@ impl Player {
pitch: f32, pitch: f32,
seed: f64, seed: f64,
) { ) {
self.client let packet = CSoundEffect::new(IdOr::Value(sound), category, position, volume, pitch, seed);
.enqueue_packet(&CSoundEffect::new( self.send_client_packet(&packet).await;
IdOr::Value(sound),
category,
position,
volume,
pitch,
seed,
))
.await;
} }
/// Stops a sound playing on the client. /// Stops a sound playing on the client.
@ -2322,9 +2302,8 @@ impl Player {
sound_id: Option<ResourceLocation>, sound_id: Option<ResourceLocation>,
category: Option<SoundCategory>, category: Option<SoundCategory>,
) { ) {
self.client let packet = CStopSound::new(sound_id, category);
.enqueue_packet(&CStopSound::new(sound_id, category)) self.send_client_packet(&packet).await;
.await;
} }
#[expect(clippy::too_many_lines)] #[expect(clippy::too_many_lines)]
@ -2348,8 +2327,7 @@ impl Player {
} else { } else {
// Target no longer exists, reset camera back to player // Target no longer exists, reset camera back to player
self.camera_target_id.store(None); self.camera_target_id.store(None);
self.client self.send_client_packet(&CSetCamera::new(self.entity_id().into()))
.send_packet_now(&CSetCamera::new(self.entity_id().into()))
.await; .await;
} }
} }
@ -2416,9 +2394,9 @@ impl Player {
&& !self.bedrock_spawned.load(Ordering::Relaxed) && !self.bedrock_spawned.load(Ordering::Relaxed)
&& total_sent_chunks > 4 && total_sent_chunks > 4
{ {
bedrock_client if let Ok(data) = bedrock_client.serialize_packet(&CPlayStatus::PlayerSpawn) {
.enqueue_packet(&CPlayStatus::PlayerSpawn) bedrock_client.enqueue_packet(data).await;
.await; }
self.bedrock_spawned.store(true, Ordering::Relaxed); self.bedrock_spawned.store(true, Ordering::Relaxed);
self.set_client_loaded(true); self.set_client_loaded(true);
self.send_health().await; self.send_health().await;
@ -2717,12 +2695,10 @@ impl Player {
if abilities.creative { if abilities.creative {
b |= 8; b |= 8;
} }
java.enqueue_packet(&CPlayerAbilities::new( let packet = CPlayerAbilities::new(b, abilities.fly_speed, abilities.walk_speed);
b, if let Ok(data) = java.serialize_packet(&packet) {
abilities.fly_speed, java.enqueue_packet(data).await;
abilities.walk_speed, }
))
.await;
} }
ClientPlatform::Bedrock(bedrock) => { ClientPlatform::Bedrock(bedrock) => {
let abilities = self.abilities.lock().await; let abilities = self.abilities.lock().await;
@ -2797,7 +2773,9 @@ impl Player {
layers, 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(); .collect();
java.enqueue_packet(&CAwardStats { let packet = CAwardStats {
stats: &packet_stats, 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, je_packet: &J,
be_packet: &B, be_packet: &B,
) { ) {
match self.client.as_ref() { self.client
ClientPlatform::Java(client) => client.enqueue_packet(je_packet).await, .enqueue_packet_editioned(je_packet, be_packet)
ClientPlatform::Bedrock(client) => client.enqueue_packet(be_packet).await, .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<C: pumpkin_protocol::ClientPacket + Sync>(&self, packet: &C) { pub async fn send_client_packet<C: pumpkin_protocol::ClientPacket + Sync>(&self, packet: &C) {
if let ClientPlatform::Java(client) = self.client.as_ref() { if let ClientPlatform::Java(client) = self.client.as_ref()
client.enqueue_packet(packet).await; && 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) { pub async fn set_compass_target(&self, pos: pumpkin_util::math::position::BlockPos) {
use pumpkin_protocol::java::client::play::CPlayerSpawnPosition; use pumpkin_protocol::java::client::play::CPlayerSpawnPosition;
self.compass_target.store(Some(pos)); self.compass_target.store(Some(pos));
self.client self.send_client_packet(&CPlayerSpawnPosition::new(pos, 0.0, 0.0, String::new()))
.enqueue_packet(&CPlayerSpawnPosition::new(pos, 0.0, 0.0, String::new()))
.await; .await;
} }
@ -3272,8 +3290,7 @@ impl Player {
level.clean_entity_chunks(&chunks_to_clean); level.clean_entity_chunks(&chunks_to_clean);
} }
for chunk in &chunks_to_clean { for chunk in &chunks_to_clean {
self.client self.send_client_packet(&CUnloadChunk::new(chunk.x, chunk.y))
.enqueue_packet(&CUnloadChunk::new(chunk.x, chunk.y))
.await; .await;
} }
@ -3352,7 +3369,7 @@ impl Player {
)); ));
match self.client.as_ref() { match self.client.as_ref() {
ClientPlatform::Java(java) => { ClientPlatform::Java(java) => {
java.send_packet_now(&CRespawn::new( let packet = CRespawn::new(
PlayerSpawnData::new( PlayerSpawnData::new(
new_world.dimension.clone(), new_world.dimension.clone(),
biome::hash_seed(new_world.level.seed.0), // seed biome::hash_seed(new_world.level.seed.0), // seed
@ -3365,7 +3382,10 @@ impl Player {
new_world.sea_level.into(), new_world.sea_level.into(),
), ),
CRespawn::KEEP_ALL_DATA, CRespawn::KEEP_ALL_DATA,
)).await; );
if let Ok(data) = java.serialize_packet(&packet) {
java.send_packet_now(data).await;
}
} }
ClientPlatform::Bedrock(bedrock) => { ClientPlatform::Bedrock(bedrock) => {
let bedrock_dimension = if new_world.dimension == Dimension::OVERWORLD { let bedrock_dimension = if new_world.dimension == Dimension::OVERWORLD {
@ -3383,7 +3403,9 @@ impl Player {
pos_f32, pos_f32,
false, 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); self.bedrock_spawned.store(false, Ordering::Relaxed);
} }
} }
@ -3440,8 +3462,7 @@ impl Player {
ClientPlatform::Java(client) => { ClientPlatform::Java(client) => {
*self.awaiting_teleport.lock().await = *self.awaiting_teleport.lock().await =
Some((teleport_id.into(), position)); Some((teleport_id.into(), position));
client let packet = CPlayerPosition::new(
.send_packet_now(&CPlayerPosition::new(
teleport_id.into(), teleport_id.into(),
position, position,
Vector3::new(0.0, 0.0, 0.0), Vector3::new(0.0, 0.0, 0.0),
@ -3449,12 +3470,13 @@ impl Player {
pitch, pitch,
// TODO // TODO
Vec::new(), Vec::new(),
)) );
.await; if let Ok(data) = client.serialize_packet(&packet) {
client.send_packet_now(data).await;
}
} }
ClientPlatform::Bedrock(client) => { ClientPlatform::Bedrock(client) => {
client let packet = CBedrockMovePlayer::new(
.send_game_packet(&CBedrockMovePlayer::new(
VarULong(self.entity_id() as u64), VarULong(self.entity_id() as u64),
Vector3::new( Vector3::new(
position.x as f32, position.x as f32,
@ -3472,8 +3494,10 @@ impl Player {
VarULong( VarULong(
self.tick_counter.load(Ordering::Relaxed).max(0) as u64, self.tick_counter.load(Ordering::Relaxed).max(0) as u64,
), ),
)) );
.await; if let Ok(data) = client.serialize_packet(&packet) {
client.send_game_packet(data).await;
}
} }
} }
} }
@ -3623,20 +3647,8 @@ impl Player {
return; return;
} }
match self.client.as_ref() {
ClientPlatform::Java(client) => {
client
.enqueue_packet(&CSetHealth::new(
self.living_entity.health.load(),
self.hunger_manager.level.load().into(),
self.hunger_manager.saturation.load(),
))
.await;
}
ClientPlatform::Bedrock(client) => {
let max_health = self.living_entity.get_max_health(); let max_health = self.living_entity.get_max_health();
let attribute = let attribute = |name: &str, current_value, max_value, default_value| BedrockAttribute {
|name: &str, current_value, max_value, default_value| BedrockAttribute {
min_value: 0.0, min_value: 0.0,
max_value, max_value,
current_value, current_value,
@ -3646,8 +3658,14 @@ impl Player {
name: name.to_string(), name: name.to_string(),
modifiers_list_size: pumpkin_protocol::codec::var_uint::VarUInt(0), modifiers_list_size: pumpkin_protocol::codec::var_uint::VarUInt(0),
}; };
client
.enqueue_packet(&CBedrockAttributes { 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), runtime_id: VarULong(self.entity_id() as u64),
attributes: vec![ attributes: vec![
attribute( attribute(
@ -3669,21 +3687,18 @@ impl Player {
5.0, 5.0,
), ),
], ],
player_tick: VarULong( player_tick: VarULong(self.tick_counter.load(Ordering::Relaxed).max(0) as u64),
self.tick_counter.load(Ordering::Relaxed).max(0) as u64 },
), )
})
.await; .await;
} }
}
}
async fn send_bedrock_respawn_state(&self, state: RespawnState) { async fn send_bedrock_respawn_state(&self, state: RespawnState) {
if let ClientPlatform::Bedrock(client) = self.client.as_ref() { if let ClientPlatform::Bedrock(client) = self.client.as_ref() {
let entity = self.get_entity(); let entity = self.get_entity();
let position = entity.pos.load(); let position = entity.pos.load();
client client
.send_game_packet(&CBedrockRespawn::new( .send_packet(&CBedrockRespawn::new(
Vector3::new( Vector3::new(
position.x as f32, position.x as f32,
position.y as f32 + entity.entity_type.eye_height, 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() { if gamemode != GameMode::Spectator && self.camera_target_id.load().is_some() {
self.camera_target_id.store(None); self.camera_target_id.store(None);
self.client.send_packet_now(&CSetCamera::new( self.send_client_packet(&CSetCamera::new(
self.entity_id().into() self.entity_id().into()
)).await; )).await;
} }
@ -4165,8 +4180,7 @@ impl Player {
sender_name: &TextComponent, sender_name: &TextComponent,
target_name: Option<&TextComponent>, target_name: Option<&TextComponent>,
) { ) {
self.client self.send_client_packet(&CDisguisedChatMessage::new(
.enqueue_packet(&CDisguisedChatMessage::new(
message, message,
(chat_type + 1).into(), (chat_type + 1).into(),
sender_name, sender_name,
@ -4283,15 +4297,9 @@ impl Player {
} }
pub async fn send_system_message_raw(&self, text: &TextComponent, overlay: bool) { pub async fn send_system_message_raw(&self, text: &TextComponent, overlay: bool) {
match self.client.as_ref() { let je_packet = CSystemChatMessage::new(text, overlay);
ClientPlatform::Java(client) => {
client
.enqueue_packet(&CSystemChatMessage::new(text, overlay))
.await;
}
ClientPlatform::Bedrock(client) => {
let locale = Locale::from_str(&self.config.load().locale).unwrap_or(Locale::EnUs); let locale = Locale::from_str(&self.config.load().locale).unwrap_or(Locale::EnUs);
let packet = match &*text.0.content { let be_packet = match &*text.0.content {
pumpkin_util::text::TextContent::Translate { pumpkin_util::text::TextContent::Translate {
translate, translate,
bedrock_translate, bedrock_translate,
@ -4306,9 +4314,7 @@ impl Player {
} }
_ => SText::system_message(text.0.to_bedrock_legacy(locale)), _ => SText::system_message(text.0.to_bedrock_legacy(locale)),
}; };
client.enqueue_packet(&packet).await; self.enqueue_packet_editioned(&je_packet, &be_packet).await;
}
}
} }
pub async fn tick_experience(&self) { pub async fn tick_experience(&self) {
@ -4323,8 +4329,7 @@ impl Player {
self.last_sent_xp.store(level, Ordering::Relaxed); self.last_sent_xp.store(level, Ordering::Relaxed);
self.client self.send_client_packet(&CSetExperience::new(
.send_packet_now(&CSetExperience::new(
progress.clamp(0.0, 1.0), progress.clamp(0.0, 1.0),
level.into(), level.into(),
points.into(), points.into(),
@ -4396,8 +4401,7 @@ impl Player {
data: &*map_data.colors, data: &*map_data.colors,
}); });
self.client self.send_client_packet(&CMapItemData {
.enqueue_packet(&CMapItemData {
map_id: VarInt(map_id), map_id: VarInt(map_id),
scale: map_data.scale, scale: map_data.scale,
locked: map_data.locked, locked: map_data.locked,
@ -4434,8 +4438,7 @@ impl Player {
self.tick_experience().await; self.tick_experience().await;
if self.has_client_loaded() { if self.has_client_loaded() {
self.client self.send_client_packet(&CSetExperience::new(
.enqueue_packet(&CSetExperience::new(
progress.clamp(0.0, 1.0), progress.clamp(0.0, 1.0),
level.into(), level.into(),
points.into(), points.into(),
@ -4510,8 +4513,7 @@ impl Player {
} }
let effect_id = VarInt(i32::from(effect.effect_type.id)); let effect_id = VarInt(i32::from(effect.effect_type.id));
self.client self.send_client_packet(&CUpdateMobEffect::new(
.enqueue_packet(&CUpdateMobEffect::new(
self.entity_id().into(), self.entity_id().into(),
effect_id, effect_id,
effect.amplifier.into(), effect.amplifier.into(),
@ -4523,8 +4525,7 @@ impl Player {
pub async fn remove_effect(&self, effect_type: &'static StatusEffect) -> bool { pub async fn remove_effect(&self, effect_type: &'static StatusEffect) -> bool {
let effect_id = VarInt(i32::from(effect_type.id)); let effect_id = VarInt(i32::from(effect_type.id));
self.client self.send_client_packet(
.enqueue_packet(
&pumpkin_protocol::java::client::play::CRemoveMobEffect::new( &pumpkin_protocol::java::client::play::CRemoveMobEffect::new(
self.entity_id().into(), self.entity_id().into(),
effect_id, effect_id,
@ -4543,8 +4544,7 @@ impl Player {
for effect in self.living_entity.active_effects.lock().await.keys() { for effect in self.living_entity.active_effects.lock().await.keys() {
effect_list.push(*effect); effect_list.push(*effect);
let effect_id = VarInt(i32::from(effect.id)); let effect_id = VarInt(i32::from(effect.id));
self.client self.send_client_packet(
.enqueue_packet(
&pumpkin_protocol::java::client::play::CRemoveMobEffect::new( &pumpkin_protocol::java::client::play::CRemoveMobEffect::new(
self.entity_id().into(), self.entity_id().into(),
effect_id, effect_id,
@ -6342,7 +6342,9 @@ impl InventoryPlayer for Player {
Box::pin(async move { Box::pin(async move {
match self.client.as_ref() { match self.client.as_ref() {
ClientPlatform::Java(java) => { ClientPlatform::Java(java) => {
java.enqueue_packet(packet).await; if let Ok(data) = java.serialize_packet(packet) {
java.enqueue_packet(data).await;
}
} }
ClientPlatform::Bedrock(bedrock) => { ClientPlatform::Bedrock(bedrock) => {
use pumpkin_protocol::bedrock::{ use pumpkin_protocol::bedrock::{
@ -6375,7 +6377,9 @@ impl InventoryPlayer for Player {
}, },
storage_item: NetworkItemStackDescriptor::default(), 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!( } else if matches!(
window_type, window_type,
Some( Some(
@ -6408,7 +6412,9 @@ impl InventoryPlayer for Player {
}, },
storage_item: NetworkItemStackDescriptor::default(), 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 { Box::pin(async move {
match self.client.as_ref() { match self.client.as_ref() {
ClientPlatform::Java(java) => { ClientPlatform::Java(java) => {
java.enqueue_packet(packet).await; if let Ok(data) = java.serialize_packet(packet) {
java.enqueue_packet(data).await;
}
} }
ClientPlatform::Bedrock(bedrock) => { ClientPlatform::Bedrock(bedrock) => {
use pumpkin_protocol::bedrock::{ use pumpkin_protocol::bedrock::{
@ -6449,7 +6457,9 @@ impl InventoryPlayer for Player {
storage: None, storage: None,
item: item_desc, item: item_desc,
}; };
bedrock.enqueue_packet(&bedrock_packet).await; if let Ok(data) = bedrock.serialize_packet(&bedrock_packet) {
bedrock.enqueue_packet(data).await;
}
} }
} else { } else {
let slot_idx = packet.slot as usize; let slot_idx = packet.slot as usize;
@ -6493,7 +6503,9 @@ impl InventoryPlayer for Player {
storage: None, storage: None,
item: item_desc, 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 { Box::pin(async move {
match self.client.as_ref() { match self.client.as_ref() {
ClientPlatform::Java(java) => { ClientPlatform::Java(java) => {
java.enqueue_packet(packet).await; if let Ok(data) = java.serialize_packet(packet) {
java.enqueue_packet(data).await;
}
} }
ClientPlatform::Bedrock(bedrock) => { ClientPlatform::Bedrock(bedrock) => {
use pumpkin_protocol::bedrock::{ use pumpkin_protocol::bedrock::{
@ -6526,7 +6540,9 @@ impl InventoryPlayer for Player {
}, },
storage_item: NetworkItemStackDescriptor::default(), 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, packet: &'a CSetContainerProperty,
) -> PlayerFuture<'a, ()> { ) -> PlayerFuture<'a, ()> {
Box::pin(async move { 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 { Box::pin(async move {
match self.client.as_ref() { match self.client.as_ref() {
ClientPlatform::Java(java) => { ClientPlatform::Java(java) => {
java.enqueue_packet(packet).await; if let Ok(data) = java.serialize_packet(packet) {
java.enqueue_packet(data).await;
}
} }
ClientPlatform::Bedrock(bedrock) => { ClientPlatform::Bedrock(bedrock) => {
use pumpkin_protocol::bedrock::{ use pumpkin_protocol::bedrock::{
@ -6576,7 +6594,9 @@ impl InventoryPlayer for Player {
storage: None, storage: None,
item: item_desc, item: item_desc,
}; };
bedrock.enqueue_packet(&bedrock_packet).await; if let Ok(data) = bedrock.serialize_packet(&bedrock_packet) {
bedrock.enqueue_packet(data).await;
}
} }
} }
}) })

View File

@ -394,8 +394,7 @@ impl PlayerAdvancement {
let first_packet = self.is_first_packet; let first_packet = self.is_first_packet;
tokio::spawn(async move { tokio::spawn(async move {
player player
.client .send_client_packet(&CUpdateAdvancements::new(
.send_packet_now(&CUpdateAdvancements::new(
first_packet, first_packet,
added, added,
parsed_progress, parsed_progress,
@ -516,8 +515,7 @@ impl PlayerAdvancement {
&& let Some(player) = self.player.upgrade() && let Some(player) = self.player.upgrade()
{ {
player player
.client .send_client_packet(&CSelectAdvancementsTab::new(
.send_packet_now(&CSelectAdvancementsTab::new(
self.last_selected_tab.map(|adv| adv.id.clone()), self.last_selected_tab.map(|adv| adv.id.clone()),
)) ))
.await; .await;

View File

@ -26,8 +26,7 @@ impl ItemBehaviour for WritableBookItem {
Box::pin(async move { Box::pin(async move {
if item.id == Item::WRITTEN_BOOK.id { if item.id == Item::WRITTEN_BOOK.id {
player player
.client .send_client_packet(&COpenBook::new(VarInt(0))) // 0 = main hand
.enqueue_packet(&COpenBook::new(VarInt(0))) // 0 = main hand
.await; .await;
player.world().play_sound( player.world().play_sound(
Sound::ItemBookPageTurn, Sound::ItemBookPageTurn,

View File

@ -18,7 +18,7 @@ use crate::net::bedrock::{
}; };
use crate::net::java::JavaClient; use crate::net::java::JavaClient;
use crate::net::java::pending::PendingConnection; 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::net::{lan_broadcast::LANBroadcast, query, rcon::RCONServer};
use crate::plugin::server::server_command::ServerCommandEvent; use crate::plugin::server::server_command::ServerCommandEvent;
use crate::server::{Server, ticker::Ticker}; use crate::server::{Server, ticker::Ticker};
@ -542,7 +542,15 @@ impl PumpkinServer {
let server_clone = self.server.clone(); let server_clone = self.server.clone();
tasks.spawn(async move { 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; let login_result = pending.handle_login_sequence(&server_clone).await;
match login_result { match login_result {
@ -609,10 +617,14 @@ impl PumpkinServer {
if let Some((session, client_addr)) = nethernet_result { if let Some((session, client_addr)) = nethernet_result {
*master_client_id_counter += 1; *master_client_id_counter += 1;
let be_clients = bedrock_clients.clone(); 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( let client = Arc::new(BedrockClient::new(
session.clone(), session.clone(),
client_addr, client_addr,
be_clients, be_clients,
packet_limiter,
)); ));
client.start_outgoing_packet_task(); client.start_outgoing_packet_task();
bedrock_clients.lock().await.insert(client_addr, client.clone()); bedrock_clients.lock().await.insert(client_addr, client.clone());

View File

@ -80,8 +80,7 @@ impl BedrockClient {
)); ));
} }
self.enqueue_packet_internal(&CPlayStatus::LoginSuccess) self.enqueue_client_packet(&CPlayStatus::LoginSuccess).await;
.await;
let br_config = &server.advanced_config.resource_pack.bedrock; let br_config = &server.advanced_config.resource_pack.bedrock;
let mut entries = Vec::new(); let mut entries = Vec::new();
@ -111,7 +110,7 @@ impl BedrockClient {
world_template_version: String::new(), world_template_version: String::new(),
resource_packs: entries, resource_packs: entries,
}; };
self.enqueue_packet_internal(&packs_info).await; self.enqueue_client_packet(&packs_info).await;
let new_config = PlayerConfig { let new_config = PlayerConfig {
locale: client_data.language_code.clone(), locale: client_data.language_code.clone(),

View File

@ -8,10 +8,10 @@ impl BedrockClient {
server: &Server, server: &Server,
) { ) {
if packet.protocol_version < CURRENT_BEDROCK_MC_PROTOCOL as i32 { if packet.protocol_version < CURRENT_BEDROCK_MC_PROTOCOL as i32 {
self.send_game_packet(&CPlayStatus::OutdatedClient).await; self.send_packet(&CPlayStatus::OutdatedClient).await;
return; return;
} else if packet.protocol_version > CURRENT_BEDROCK_MC_PROTOCOL as i32 { } 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; return;
} }
@ -26,7 +26,7 @@ impl BedrockClient {
.compression .compression
.info .info
.clone(); .clone();
self.send_game_packet(&CNetworkSettings::new( self.send_packet(&CNetworkSettings::new(
compression.threshold as u16, compression.threshold as u16,
0, 0,
false, false,

View File

@ -40,7 +40,7 @@ impl BedrockClient {
Vec::new() Vec::new()
}; };
self.enqueue_packet_internal(&CResourcePackStackPacket::new( self.enqueue_client_packet(&CResourcePackStackPacket::new(
br_config.force, br_config.force,
resource_packs, resource_packs,
CURRENT_BEDROCK_MC_VERSION.to_string(), CURRENT_BEDROCK_MC_VERSION.to_string(),

View File

@ -58,7 +58,7 @@ pub mod login;
use self::nethernet::NetherNetSession; use self::nethernet::NetherNetSession;
use crate::{ use crate::{
entity::player::Player, entity::player::Player,
net::{DisconnectReason, PacketHandlerResult}, net::{DisconnectReason, PacketHandlerResult, PacketRateLimiter},
plugin::api::events::world::chunk_send::ChunkSend, plugin::api::events::world::chunk_send::ChunkSend,
server::Server, server::Server,
}; };
@ -123,6 +123,8 @@ pub struct BedrockClient {
last_seen: Arc<AtomicCell<std::time::Instant>>, last_seen: Arc<AtomicCell<std::time::Instant>>,
incoming_game_packet_send: Sender<RawPacket>, incoming_game_packet_send: Sender<RawPacket>,
incoming_game_packet_recv: Mutex<Option<Receiver<RawPacket>>>, incoming_game_packet_recv: Mutex<Option<Receiver<RawPacket>>>,
/// Packet rate limiter for incoming client packets.
pub packet_limiter: PacketRateLimiter,
} }
impl BedrockClient { impl BedrockClient {
@ -131,6 +133,7 @@ impl BedrockClient {
session: Arc<NetherNetSession>, session: Arc<NetherNetSession>,
address: SocketAddr, address: SocketAddr,
be_clients: Arc<Mutex<HashMap<SocketAddr, Arc<Self>>>>, be_clients: Arc<Mutex<HashMap<SocketAddr, Arc<Self>>>>,
packet_limiter: PacketRateLimiter,
) -> Self { ) -> Self {
let (send, recv) = tokio::sync::mpsc::channel(4096); let (send, recv) = tokio::sync::mpsc::channel(4096);
let (priority_send, priority_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())), last_seen: Arc::new(AtomicCell::new(std::time::Instant::now())),
incoming_game_packet_send: incoming_send, incoming_game_packet_send: incoming_send,
incoming_game_packet_recv: Mutex::new(Some(incoming_recv)), 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) { 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; .await;
self.close().await; self.close().await;
} }
@ -280,7 +284,7 @@ impl BedrockClient {
send_packet: bool, send_packet: bool,
) { ) {
if send_packet { if send_packet {
self.send_game_packet(&CDisconnectPlayer { self.send_packet(&CDisconnectPlayer {
reason: pumpkin_protocol::codec::var_int::VarInt(reason as i32), reason: pumpkin_protocol::codec::var_int::VarInt(reason as i32),
skip_message, skip_message,
message, message,
@ -383,62 +387,12 @@ impl BedrockClient {
self.player.store(Arc::new(Some(player))); self.player.store(Arc::new(Some(player)));
} }
pub async fn enqueue_packet<P: BClientPacket>(&self, packet: &P) { pub async fn enqueue_packet(&self, packet_data: Bytes) {
let mut packet_buf = Vec::new(); self.enqueue_packet_data(packet_data).await;
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_internal<P: BClientPacket>(&self, packet: &P) { pub fn try_enqueue_packet(&self, packet_data: Bytes) {
let mut packet_buf = Vec::new(); self.try_enqueue_packet_data(packet_data);
match self.write_game_packet(packet, &mut packet_buf).await {
Ok(()) => self.enqueue_packet_data(packet_buf.into()).await,
Err(err) => error!("Failed to write game packet: {err}"),
}
}
pub fn try_enqueue_packet<P: BClientPacket>(&self, packet: &P) {
let mut packet_buf = Vec::new();
let mut packet_payload = Vec::new();
if let Err(err) = packet.write_packet(&mut packet_payload) {
error!("Failed to write packet for try_enqueue_packet: {err}");
return;
}
{
let Ok(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());
} }
/// Queues a clientbound packet to be sent to the connected client. Queued chunks are sent /// Queues a clientbound packet to be sent to the connected client. Queued chunks are sent
@ -446,7 +400,7 @@ impl BedrockClient {
/// ///
/// # Arguments /// # 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) { pub async fn enqueue_packet_data(&self, packet_data: Bytes) {
if let Err(err) = self if let Err(err) = self
.outgoing_packet_queue_send .outgoing_packet_queue_send
@ -508,27 +462,31 @@ impl BedrockClient {
) )
} }
pub async fn send_game_packet<P: BClientPacket>(&self, packet: &P) { pub fn serialize_packet<P: BClientPacket>(&self, packet: &P) -> Result<Bytes, Error> {
let mut packet_buf = Vec::new(); let encoder = self.network_writer.try_read();
match self.write_game_packet(packet, &mut packet_buf).await { encoder.map_or_else(
Ok(()) => { |_| pumpkin_protocol::bedrock::packet_encoder::serialize_packet(packet),
let payload = Bytes::from(packet_buf); |encoder| encoder.serialize_packet(packet),
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 { pub async fn send_packet<P: BClientPacket>(&self, packet: &P) {
payload if let Ok(data) = self.serialize_packet(packet) {
}; self.send_game_packet(data).await;
}
}
pub async fn enqueue_client_packet<P: BClientPacket>(&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(); let (tx, rx) = oneshot::channel();
if let Err(err) = self if let Err(err) = self
.outgoing_packet_priority_send .outgoing_packet_priority_send
.send(OutgoingPacket::priority(payload, tx)) .send(OutgoingPacket::priority(packet_data, tx))
.await .await
{ {
if !self.is_closed() { if !self.is_closed() {
@ -538,9 +496,6 @@ impl BedrockClient {
let _ = rx.await; let _ = rx.await;
} }
} }
Err(err) => error!("Failed to write game packet: {err}"),
}
}
pub async fn close(&self) { pub async fn close(&self) {
if self.close_token.is_cancelled() { if self.close_token.is_cancelled() {
@ -585,6 +540,27 @@ impl BedrockClient {
.await .await
.get_game_packet(&mut cursor) .get_game_packet(&mut cursor)
.map_err(|e| Error::other(e.to_string()))?; .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?; self.handle_game_packet(server, game_packet).await?;
} }
@ -811,7 +787,7 @@ impl BedrockClient {
} }
} }
if !missing_blobs.is_empty() { if !missing_blobs.is_empty() {
self.send_game_packet(&CClientCacheMissResponse { self.send_packet(&CClientCacheMissResponse {
blobs: &missing_blobs, blobs: &missing_blobs,
}) })
.await; .await;

View File

@ -90,7 +90,7 @@ impl BedrockClient {
player.living_entity.send_equipment_changes(equipment); player.living_entity.send_equipment_changes(equipment);
// Sync bedrock inventory updates // Sync bedrock inventory updates
self.enqueue_packet(&CInventoryContent { self.enqueue_client_packet(&CInventoryContent {
container_id: VarUInt(0), container_id: VarUInt(0),
slots: player slots: player
.inventory() .inventory()

View File

@ -8,7 +8,7 @@ impl BedrockClient {
} }
player.on_handled_screen_closed().await; player.on_handled_screen_closed().await;
self.enqueue_packet(&SContainerClose { self.enqueue_client_packet(&SContainerClose {
container_id: packet.container_id, container_id: packet.container_id,
container_type: packet.container_type, container_type: packet.container_type,
server_initiated: false, server_initiated: false,
@ -16,7 +16,7 @@ impl BedrockClient {
.await; .await;
// Sync the cursor (make it empty) to Bedrock client // 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 container_id: VarUInt(59), // Cursor container ID
slots: vec![NetworkItemStackDescriptor::default()], slots: vec![NetworkItemStackDescriptor::default()],
full_container_name: FullContainerName { full_container_name: FullContainerName {
@ -28,7 +28,7 @@ impl BedrockClient {
.await; .await;
// Sync the inventory content to Bedrock client // Sync the inventory content to Bedrock client
self.enqueue_packet(&CInventoryContent { self.enqueue_client_packet(&CInventoryContent {
container_id: VarUInt(0), // player inventory container_id: VarUInt(0), // player inventory
slots: player slots: player
.inventory() .inventory()

View File

@ -30,7 +30,7 @@ impl BedrockClient {
return; return;
} }
self.inventory_opened.store(true, Ordering::Relaxed); self.inventory_opened.store(true, Ordering::Relaxed);
self.enqueue_packet(&CContainerOpen { self.enqueue_client_packet(&CContainerOpen {
container_id: 0, container_id: 0,
container_type: 0xff, container_type: 0xff,
position: BlockPos::ZERO, position: BlockPos::ZERO,

View File

@ -119,7 +119,7 @@ impl BedrockClient {
} }
if inventory_updated { if inventory_updated {
self.enqueue_packet(&CInventoryContent { self.enqueue_client_packet(&CInventoryContent {
container_id: VarUInt(0), container_id: VarUInt(0),
slots: player slots: player
.inventory() .inventory()
@ -444,7 +444,7 @@ impl BedrockClient {
} }
} }
self.enqueue_packet(&CItemStackResponse { self.enqueue_client_packet(&CItemStackResponse {
responses: vec![ItemStackResponse { responses: vec![ItemStackResponse {
result, result,
request_id: packet.legacy_request_id, request_id: packet.legacy_request_id,

View File

@ -514,10 +514,11 @@ impl BedrockClient {
} }
// Send Bedrock specific responses and updates // Send Bedrock specific responses and updates
self.enqueue_packet(&CItemStackResponse { responses }).await; self.enqueue_client_packet(&CItemStackResponse { responses })
.await;
if inventory_updated { if inventory_updated {
self.enqueue_packet(&CInventoryContent { self.enqueue_client_packet(&CInventoryContent {
container_id: VarUInt(0), container_id: VarUInt(0),
slots: player slots: player
.inventory() .inventory()

View File

@ -23,7 +23,7 @@ impl BedrockClient {
event.cancelled() event.cancelled()
}; };
if is_cancelled { if is_cancelled {
self.enqueue_packet(&CPlayerHotbar { self.enqueue_client_packet(&CPlayerHotbar {
selected_slot: VarUInt(previous_slot as u32), selected_slot: VarUInt(previous_slot as u32),
container_id: 0, container_id: 0,
should_select_block: true, should_select_block: true,

View File

@ -167,7 +167,7 @@ impl BedrockClient {
} }
} else { } else {
let runtime_id = pumpkin_data::BlockState::to_be_network_id(state.id); 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; .await;
if matches!(action, PlayerAction::StopBreak) { if matches!(action, PlayerAction::StopBreak) {
player.stop_mining().await; player.stop_mining().await;

View File

@ -25,7 +25,7 @@ impl BedrockClient {
NonZero::<i32>::from(server.advanced_config.networking.bedrock.view_distance).get(), NonZero::<i32>::from(server.advanced_config.networking.bedrock.view_distance).get(),
); );
self.enqueue_packet(&CChunkRadiusUpdate { self.enqueue_client_packet(&CChunkRadiusUpdate {
chunk_radius: VarInt(view_distance), chunk_radius: VarInt(view_distance),
}) })
.await; .await;

View File

@ -12,7 +12,7 @@ impl BedrockClient {
let entity = player.get_entity(); let entity = player.get_entity();
let position = entity.pos.load(); let position = entity.pos.load();
self.enqueue_packet(&CRespawn::new( self.enqueue_client_packet(&CRespawn::new(
pumpkin_util::math::vector3::Vector3::new( pumpkin_util::math::vector3::Vector3::new(
position.x as f32, position.x as f32,
position.y as f32 + entity.entity_type.eye_height, position.y as f32 + entity.entity_type.eye_height,

View File

@ -2,17 +2,24 @@
use super::*; use super::*;
impl JavaClient { impl JavaClient {
pub async fn handle_config_keep_alive(&self, keep_alive: SKeepAlive) { pub fn handle_config_keep_alive(&self, keep_alive: &SKeepAlive) {
if self.wait_for_keep_alive.load(Ordering::Relaxed) let mut pending = self
&& keep_alive.keep_alive_id == self.keep_alive_id.load() .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); self.wait_for_keep_alive.store(false, Ordering::Relaxed);
} else { } else {
self.kick(pumpkin_macros::translate_cross!( debug!(
translation::java::DISCONNECT_TIMEOUT, "Ignored unexpected config keep alive id {}",
translation::bedrock::DISCONNECT_TIMEOUT keep_alive.keep_alive_id
)) );
.await;
} }
} }
} }

View File

@ -11,7 +11,7 @@ impl JavaClient {
// let mut tags_to_send = Vec::new(); // let mut tags_to_send = Vec::new();
let version = self.version.load(); let version = self.version.load();
if version >= JavaMinecraftVersion::V_1_20_2 { 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; .await;
let registry = Registry::get_synced(version); let registry = Registry::get_synced(version);
let mut sent_dimension_type = false; let mut sent_dimension_type = false;
@ -19,7 +19,7 @@ impl JavaClient {
if reg.registry_id == "minecraft:dimension_type" { if reg.registry_id == "minecraft:dimension_type" {
sent_dimension_type = true; sent_dimension_type = true;
} }
self.send_packet_now(&CRegistryData::new(&reg.registry_id, &reg.registry_entries)) self.send_packet(&CRegistryData::new(&reg.registry_id, &reg.registry_entries))
.await; .await;
} }
if !sent_dimension_type { if !sent_dimension_type {
@ -36,7 +36,7 @@ impl JavaClient {
data: Some(build_dimension_nbt(dim).into_boxed_slice()), data: Some(build_dimension_nbt(dim).into_boxed_slice()),
}) })
.collect(); .collect();
self.send_packet_now(&CRegistryData::new( self.send_packet(&CRegistryData::new(
&"minecraft:dimension_type".to_string(), &"minecraft:dimension_type".to_string(),
&dim_entries, &dim_entries,
)) ))
@ -71,10 +71,10 @@ impl JavaClient {
tags.push(key); tags.push(key);
} }
} }
self.send_packet_now(&CUpdateTags::new(&tags)).await; self.send_packet(&CUpdateTags::new(&tags)).await;
// We are done with configuring // We are done with configuring
self.send_packet_now(&CFinishConfig).await; self.send_packet(&CFinishConfig).await;
if version < JavaMinecraftVersion::V_1_20_2 { if version < JavaMinecraftVersion::V_1_20_2 {
return Some(self.handle_config_acknowledged(server).await); return Some(self.handle_config_acknowledged(server).await);

View File

@ -9,7 +9,7 @@ use crate::{
server::Server, server::Server,
}; };
use core::str; use core::str;
use pumpkin_data::{registry::Registry, translation}; use pumpkin_data::registry::Registry;
use pumpkin_protocol::{ use pumpkin_protocol::{
ConnectionState, KnownPack, ConnectionState, KnownPack,
java::{ java::{

View File

@ -74,7 +74,7 @@ impl JavaClient {
pub async fn send_known_packs(&self) { pub async fn send_known_packs(&self) {
let version_str = self.version.load().to_string(); let version_str = self.version.load().to_string();
self.send_packet_now(&CKnownPacks::new(&[KnownPack { self.send_packet(&CKnownPacks::new(&[KnownPack {
namespace: "minecraft", namespace: "minecraft",
id: "core", id: "core",
version: &version_str, version: &version_str,

View File

@ -63,7 +63,7 @@ use arc_swap::ArcSwap;
use pending::PendingConnection; use pending::PendingConnection;
use crate::entity::player::Player; 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::api::events::world::chunk_send::ChunkSend;
use crate::plugin::player::player_custom_payload::PlayerCustomPayloadEvent; use crate::plugin::player::player_custom_payload::PlayerCustomPayloadEvent;
use crate::{error::PumpkinError, server::Server}; use crate::{error::PumpkinError, server::Server};
@ -109,8 +109,14 @@ pub struct JavaClient {
pub keep_alive_id: AtomicCell<i64>, pub keep_alive_id: AtomicCell<i64>,
/// The last time we sent a keep alive packet. /// The last time we sent a keep alive packet.
pub last_keep_alive_time: AtomicCell<Instant>, pub last_keep_alive_time: AtomicCell<Instant>,
/// The last time any packet was received from the client.
pub last_packet_time: AtomicCell<Instant>,
/// Recent in-flight keep alive IDs with their sent timestamps.
pub pending_keep_alives: std::sync::Mutex<Vec<(i64, Instant)>>,
pub packet_sequence: AtomicI32, pub packet_sequence: AtomicI32,
/// Packet rate limiter for incoming client packets.
pub packet_limiter: PacketRateLimiter,
} }
pub enum OutgoingPacketType { pub enum OutgoingPacketType {
@ -170,7 +176,10 @@ impl JavaClient {
wait_for_keep_alive: AtomicBool::new(false), wait_for_keep_alive: AtomicBool::new(false),
keep_alive_id: AtomicCell::new(0), keep_alive_id: AtomicCell::new(0),
last_keep_alive_time: AtomicCell::new(Instant::now()), 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_sequence: AtomicI32::new(-1),
packet_limiter: pending.packet_limiter,
} }
} }
@ -178,6 +187,7 @@ impl JavaClient {
self.player.store(Arc::new(Some(player))); self.player.store(Arc::new(Some(player)));
} }
#[expect(clippy::too_many_lines)]
pub async fn progress_player_packets(&self, player: &Arc<Player>, server: &Arc<Server>) { pub async fn progress_player_packets(&self, player: &Arc<Player>, server: &Arc<Server>) {
let Some(mut network_reader) = self let Some(mut network_reader) = self
.network_reader .network_reader
@ -188,7 +198,11 @@ impl JavaClient {
return; 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 // Skip the immediate first tick so we don't send a keep-alive the exact millisecond they join
keep_alive_interval.tick().await; keep_alive_interval.tick().await;
@ -197,8 +211,17 @@ impl JavaClient {
tokio::select! { tokio::select! {
// KEEP-ALIVE TIMER // KEEP-ALIVE TIMER
_ = keep_alive_interval.tick() => { _ = keep_alive_interval.tick() => {
// If the client never responded to the LAST keep-alive, they timed out. // Check if the client has timed out on keep-alive responses or no packet activity
if self.wait_for_keep_alive.load(Ordering::Relaxed) { 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; self.kick(pumpkin_macros::translate_cross!(translation::java::DISCONNECT_TIMEOUT, translation::bedrock::DISCONNECT_TIMEOUT)).await;
break; break;
} }
@ -212,8 +235,18 @@ impl JavaClient {
self.keep_alive_id.store(keep_alive_id); self.keep_alive_id.store(keep_alive_id);
self.wait_for_keep_alive.store(true, Ordering::Relaxed); self.wait_for_keep_alive.store(true, Ordering::Relaxed);
self.last_keep_alive_time.store(Instant::now()); 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); 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 // INCOMING PACKETS
@ -221,6 +254,27 @@ impl JavaClient {
let Some(packet) = packet_opt else { let Some(packet) = packet_opt else {
break; 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 { match self.handle_play_packet(player, server, &packet).await {
Ok(()) => {} Ok(()) => {}
@ -249,7 +303,7 @@ impl JavaClient {
// it interacted with and drops our updates for that position // it interacted with and drops our updates for that position
let seq = self.packet_sequence.swap(-1, Ordering::Relaxed); let seq = self.packet_sequence.swap(-1, Ordering::Relaxed);
if seq != -1 { if seq != -1 {
self.send_packet_now(&CAcknowledgeBlockChange::new(seq.into())) self.send_packet(&CAcknowledgeBlockChange::new(seq.into()))
.await; .await;
} }
} }
@ -282,10 +336,6 @@ impl JavaClient {
pub async fn send_chunks(&self, chunks: &[SyncChunk]) { pub async fn send_chunks(&self, chunks: &[SyncChunk]) {
let player = self.player.load_full(); let player = self.player.load_full();
let Some(player) = player.as_ref() else { let Some(player) = player.as_ref() else {
debug!(
"send_chunks: player not set yet, dropping {} chunks",
chunks.len()
);
return; return;
}; };
let Some(server) = player.world().server.upgrade() else { let Some(server) = player.world().server.upgrade() else {
@ -293,7 +343,7 @@ impl JavaClient {
}; };
if self.version.load() >= JavaMinecraftVersion::V_1_20_2 { if self.version.load() >= JavaMinecraftVersion::V_1_20_2 {
self.send_packet_now(&CChunkBatchStart).await; self.send_packet(&CChunkBatchStart).await;
} }
for chunk in chunks { for chunk in chunks {
let mut event = ChunkSend::new(player.world(), chunk.clone()); let mut event = ChunkSend::new(player.world(), chunk.clone());
@ -315,55 +365,15 @@ impl JavaClient {
self.send_packet_now_data(buf.into()).await; self.send_packet_now_data(buf.into()).await;
} }
if self.version.load() >= JavaMinecraftVersion::V_1_20_2 { 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; .await;
} }
} }
pub async fn enqueue_packet<P: ClientPacket>(&self, packet: &P) { pub async fn enqueue_packet(&self, packet_data: Bytes) {
let mut buf = Vec::new(); self.enqueue_packet_data(packet_data).await;
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 fn try_enqueue_packet<P: ClientPacket>(&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) { pub async fn enqueue_packet_data(&self, packet_data: Bytes) {
if let Err(err) = self if let Err(err) = self
.outgoing_packet_queue_send .outgoing_packet_queue_send
@ -372,14 +382,21 @@ impl JavaClient {
{ {
// This is expected to fail if we are closed // This is expected to fail if we are closed
if !self.close_token.is_cancelled() { if !self.close_token.is_cancelled() {
error!( warn!(
"Failed to add packet to the outgoing packet queue for client {}: {}", "Failed to add packet to the outgoing packet queue for client {}: {}",
self.id, err 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) { pub fn try_enqueue_packet_data(&self, packet_data: Bytes) {
if let Err(err) = self if let Err(err) = self
.outgoing_packet_queue_send .outgoing_packet_queue_send
@ -394,10 +411,11 @@ impl JavaClient {
} }
tokio::sync::mpsc::error::TrySendError::Closed(_) => { tokio::sync::mpsc::error::TrySendError::Closed(_) => {
if !self.close_token.is_cancelled() { if !self.close_token.is_cancelled() {
error!( warn!(
"Failed to add packet to the outgoing packet queue for client {}: channel closed", "Failed to add packet to the outgoing packet queue for client {}: channel closed",
self.id self.id
); );
self.close();
} }
} }
} }
@ -442,16 +460,16 @@ impl JavaClient {
match self.connection_state.load() { match self.connection_state.load() {
ConnectionState::Login => { ConnectionState::Login => {
// TextComponent implements Serialize and writes in bytes instead of String, that's the reason we only use content // 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()), serde_json::to_string(&reason.0).unwrap_or_else(|_| String::new()),
)) ))
.await; .await;
} }
ConnectionState::Config => { ConnectionState::Config => {
self.send_packet_now(&CConfigDisconnect::new(&reason.clone().get_text())) self.send_packet(&CConfigDisconnect::new(&reason.clone().get_text()))
.await; .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(); self.close();
} }
pub async fn send_packet_now<P: ClientPacket>(&self, packet: &P) { pub async fn send_packet_now(&self, packet: Bytes) {
let mut packet_buf = Vec::new(); self.send_packet_now_data(packet).await;
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_data(&self, packet: Bytes) { pub async fn send_packet_now_data(&self, packet: Bytes) {
@ -514,24 +511,32 @@ impl JavaClient {
pub fn write_packet_for_version<P: ClientPacket>( pub fn write_packet_for_version<P: ClientPacket>(
packet: &P, packet: &P,
version: JavaMinecraftVersion, version: JavaMinecraftVersion,
mut write: impl Write, write: impl Write,
) -> Result<(), WritingError> { ) -> Result<(), WritingError> {
let version_number = P::to_id(version); pumpkin_protocol::java::packet_encoder::write_packet(packet, &version, write)
if version_number == -1 {
return Ok(());
}
write.write_var_int(&VarInt(version_number))?;
packet.write_packet_data(write, &version)
} }
pub fn serialize_packet_for_version<P: ClientPacket>( pub fn serialize_packet_for_version<P: ClientPacket>(
packet: &P, packet: &P,
version: JavaMinecraftVersion, version: JavaMinecraftVersion,
) -> Result<Bytes, WritingError> { ) -> Result<Bytes, WritingError> {
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)?; pub fn serialize_packet<P: ClientPacket>(&self, packet: &P) -> Result<Bytes, WritingError> {
Ok(packet_buf.into()) Self::serialize_packet_for_version(packet, self.version.load())
}
pub async fn send_packet<P: ClientPacket>(&self, packet: &P) {
if let Ok(data) = self.serialize_packet(packet) {
self.send_packet_now(data).await;
}
}
pub async fn enqueue_client_packet<P: ClientPacket>(&self, packet: &P) {
if let Ok(data) = self.serialize_packet(packet) {
self.enqueue_packet(data).await;
}
} }
pub fn write_packet<P: ClientPacket>( pub fn write_packet<P: ClientPacket>(
@ -761,9 +766,11 @@ impl JavaClient {
id if id == pumpkin_protocol::java::server::play::SKeepAlive::to_id(version) => { id if id == pumpkin_protocol::java::server::play::SKeepAlive::to_id(version) => {
self.handle_keep_alive( self.handle_keep_alive(
player, player,
pumpkin_protocol::java::server::play::SKeepAlive::read(&mut payload, &version)?, &pumpkin_protocol::java::server::play::SKeepAlive::read(
) &mut payload,
.await; &version,
)?,
);
} }
id if id == SClientTickEnd::to_id(version) => { id if id == SClientTickEnd::to_id(version) => {
// TODO // TODO

View File

@ -33,7 +33,10 @@ use tracing::{debug, error, warn};
use crate::{ use crate::{
entity::player::ChatMode, entity::player::ChatMode,
net::{EncryptionError, GameProfile, PacketHandlerResult, PlayerConfig, can_not_join}, net::{
EncryptionError, GameProfile, PacketHandlerResult, PacketRateLimiter, PlayerConfig,
can_not_join,
},
server::Server, server::Server,
}; };
@ -53,11 +56,17 @@ pub struct PendingConnection {
pub gameprofile: Option<GameProfile>, pub gameprofile: Option<GameProfile>,
pub config: Option<PlayerConfig>, pub config: Option<PlayerConfig>,
pub brand: Option<String>, pub brand: Option<String>,
pub packet_limiter: PacketRateLimiter,
} }
impl PendingConnection { impl PendingConnection {
#[must_use] #[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(); let (read, write) = tcp_stream.into_split();
Self { Self {
id, id,
@ -71,6 +80,7 @@ impl PendingConnection {
gameprofile: None, gameprofile: None,
config: None, config: None,
brand: None, brand: None,
packet_limiter,
} }
} }
@ -172,6 +182,25 @@ impl PendingConnection {
pub async fn handle_login_sequence(&mut self, server: &Arc<Server>) -> PacketHandlerResult { pub async fn handle_login_sequence(&mut self, server: &Arc<Server>) -> PacketHandlerResult {
while let Some(packet) = self.get_packet().await { 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 { match self.handle_packet(server, &packet).await {
Ok(result) => { Ok(result) => {
if let Some(result) = result { if let Some(result) = result {

View File

@ -31,6 +31,6 @@ impl JavaClient {
suggestions.into(), suggestions.into(),
); );
self.enqueue_packet(&response).await; self.enqueue_client_packet(&response).await;
} }
} }

View File

@ -36,10 +36,7 @@ impl JavaClient {
if let Some(target) = target { if let Some(target) = target {
if player.gamemode.load() == GameMode::Spectator { if player.gamemode.load() == GameMode::Spectator {
player.camera_target_id.store(Some(entity_id.0)); player.camera_target_id.store(Some(entity_id.0));
player player.send_client_packet(&CSetCamera::new(entity_id)).await;
.client
.send_packet_now(&CSetCamera::new(entity_id))
.await;
return; return;
} }
send_cancellable! {{ send_cancellable! {{

View File

@ -2,23 +2,35 @@
use super::*; use super::*;
impl JavaClient { impl JavaClient {
pub async fn handle_keep_alive(&self, player: &Player, keep_alive: SKeepAlive) { pub fn handle_keep_alive(&self, player: &Player, keep_alive: &SKeepAlive) {
if self.wait_for_keep_alive.load(Ordering::Relaxed) let mut pending = self
&& keep_alive.keep_alive_id == self.keep_alive_id.load() .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 // Vanilla logic
player.ping.store( 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, Ordering::Relaxed,
); );
self.wait_for_keep_alive.store(false, Ordering::Relaxed); self.wait_for_keep_alive.store(false, Ordering::Relaxed);
} else { } else {
self.kick(pumpkin_macros::translate_cross!( debug!(
translation::java::DISCONNECT_TIMEOUT, "Ignored unexpected or duplicate keep alive id {} from player {}",
translation::bedrock::DISCONNECT_TIMEOUT keep_alive.keep_alive_id, player.gameprofile.name
)) );
.await;
} }
} }
} }

View File

@ -40,8 +40,7 @@ impl JavaClient {
} }
player player
.client .send_client_packet(&CSetSelectedSlot::new(
.enqueue_packet(&CSetSelectedSlot::new(
player.inventory.get_selected_slot() as i8 player.inventory.get_selected_slot() as i8
)) ))
.await; .await;
@ -104,8 +103,7 @@ impl JavaClient {
} }
player player
.client .send_client_packet(&CSetSelectedSlot::new(
.enqueue_packet(&CSetSelectedSlot::new(
player.inventory.get_selected_slot() as i8 player.inventory.get_selected_slot() as i8
)) ))
.await; .await;

View File

@ -3,7 +3,7 @@ use super::*;
impl JavaClient { impl JavaClient {
pub async fn handle_play_ping_request(&self, request: SPlayPingRequest) { 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; .await;
} }
} }

View File

@ -66,7 +66,7 @@ impl JavaClient {
let inventory = player.inventory(); let inventory = player.inventory();
let held = inventory.held_item().await; let held = inventory.held_item().await;
if !server.item_registry.can_mine(held.item, player) { if !server.item_registry.can_mine(held.item, player) {
self.enqueue_packet(&CBlockUpdate::new( self.enqueue_client_packet(&CBlockUpdate::new(
position, position,
VarInt(i32::from(state.id.as_u16())), 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) { async fn sync_block_state_to_client(&self, world: &World, position: BlockPos) {
let synced_state_id = world.get_block_state_id(&position); let synced_state_id = world.get_block_state_id(&position);
self.send_packet_now(&CBlockUpdate::new( self.send_packet(&CBlockUpdate::new(
position, position,
VarInt(i32::from(synced_state_id.as_u16())), VarInt(i32::from(synced_state_id.as_u16())),
)) ))

View File

@ -27,8 +27,7 @@ impl JavaClient {
{ {
player.camera_target_id.store(None); player.camera_target_id.store(None);
player player
.client .send_client_packet(&CSetCamera::new(player.entity_id().into()))
.send_packet_now(&CSetCamera::new(player.entity_id().into()))
.await; .await;
} }

View File

@ -334,7 +334,7 @@ impl JavaClient {
pub async fn force_tp(&self, player: &Arc<Player>, position: Vector3<f64>) { pub async fn force_tp(&self, player: &Arc<Player>, position: Vector3<f64>) {
let teleport_id = player.teleport_id_count.fetch_add(1, Ordering::Relaxed) + 1; let teleport_id = player.teleport_id_count.fetch_add(1, Ordering::Relaxed) + 1;
*player.awaiting_teleport.lock().await = Some((teleport_id.into(), position)); *player.awaiting_teleport.lock().await = Some((teleport_id.into(), position));
self.enqueue_packet(&CPlayerPosition::new( self.enqueue_client_packet(&CPlayerPosition::new(
teleport_id.into(), teleport_id.into(),
player.get_entity().pos.load(), player.get_entity().pos.load(),
Vector3::new(0.0, 0.0, 0.0), Vector3::new(0.0, 0.0, 0.0),

View File

@ -24,8 +24,7 @@ impl JavaClient {
server.plugin_manager.fire(server, &mut event).await; server.plugin_manager.fire(server, &mut event).await;
if event.cancelled { if event.cancelled {
player player
.client .send_client_packet(&CSetSelectedSlot::new(previous_slot as i8))
.enqueue_packet(&CSetSelectedSlot::new(previous_slot as i8))
.await; .await;
return; return;
} }

View File

@ -28,8 +28,7 @@ impl JavaClient {
player.camera_target_id.store(Some(target_id)); player.camera_target_id.store(Some(target_id));
player player
.client .send_client_packet(&CSetCamera::new(target_id.into()))
.send_packet_now(&CSetCamera::new(target_id.into()))
.await; .await;
player player
@ -43,8 +42,7 @@ impl JavaClient {
player.camera_target_id.store(Some(target_id)); player.camera_target_id.store(Some(target_id));
player player
.client .send_client_packet(&CSetCamera::new(target_id.into()))
.send_packet_now(&CSetCamera::new(target_id.into()))
.await; .await;
player player

View File

@ -17,7 +17,7 @@ impl JavaClient {
// Empty NBT compound (TAG_End = 0) // Empty NBT compound (TAG_End = 0)
let empty_nbt = [0u8]; 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; .await;
} }
@ -28,7 +28,7 @@ impl JavaClient {
// Empty NBT compound (TAG_End = 0) // Empty NBT compound (TAG_End = 0)
let empty_nbt = [0u8]; 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; .await;
} }
} }

View File

@ -25,8 +25,7 @@ impl JavaClient {
let target_id = target_player.living_entity.entity.entity_id; let target_id = target_player.living_entity.entity.entity_id;
player.camera_target_id.store(Some(target_id)); player.camera_target_id.store(Some(target_id));
player player
.client .send_client_packet(&CSetCamera::new(target_id.into()))
.send_packet_now(&CSetCamera::new(target_id.into()))
.await; .await;
player player

View File

@ -66,7 +66,7 @@ impl JavaClient {
event; event;
'cancelled: { 'cancelled: {
let state_id = world.get_block_state_id(&position); let state_id = world.get_block_state_id(&position);
self.enqueue_packet(&CBlockUpdate::new( self.enqueue_client_packet(&CBlockUpdate::new(
position, position,
VarInt(i32::from(state_id.as_u16())), VarInt(i32::from(state_id.as_u16())),
)) ))
@ -237,7 +237,7 @@ impl JavaClient {
.await .await
{ {
Ok(Some((final_block_pos, new_state))) => { Ok(Some((final_block_pos, new_state))) => {
self.send_packet_now(&CBlockUpdate::new( self.send_packet(&CBlockUpdate::new(
final_block_pos, final_block_pos,
VarInt(i32::from(new_state.as_u16())), 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. /// 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) { 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; .await;
} }
} }

View File

@ -30,6 +30,8 @@ pub mod authentication;
pub mod bedrock; pub mod bedrock;
pub mod java; pub mod java;
pub mod lan_broadcast; pub mod lan_broadcast;
pub mod packet_limiter;
pub use packet_limiter::PacketRateLimiter;
mod proxy; mod proxy;
pub mod query; pub mod query;
pub mod rcon; pub mod rcon;
@ -202,8 +204,16 @@ impl ClientPlatform {
be_packet: &B, be_packet: &B,
) { ) {
match self { match self {
Self::Java(java) => java.enqueue_packet(je_packet).await, Self::Java(java) => {
Self::Bedrock(bedrock) => bedrock.enqueue_packet(be_packet).await, 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, be_packet: &B,
) { ) {
match self { match self {
Self::Java(java) => java.try_enqueue_packet(je_packet), Self::Java(java) => {
Self::Bedrock(bedrock) => bedrock.try_enqueue_packet(be_packet), 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<P: ClientPacket>(&self, packet: &P) { pub async fn enqueue_packet(&self, packet_data: Bytes) {
if let Self::Java(java) = self { match self {
java.enqueue_packet(packet).await; Self::Java(java) => java.enqueue_packet(packet_data).await,
Self::Bedrock(bedrock) => bedrock.enqueue_packet(packet_data).await,
} }
} }
pub async fn enqueue_be_packet<P: BClientPacket>(&self, packet: &P) { pub fn try_enqueue_packet(&self, packet_data: Bytes) {
if let Self::Bedrock(bedrock) = self { match self {
bedrock.enqueue_packet(packet).await; Self::Java(java) => java.try_enqueue_packet(packet_data),
} Self::Bedrock(bedrock) => bedrock.try_enqueue_packet(packet_data),
}
pub fn try_enqueue_packet<P: ClientPacket>(&self, packet: &P) {
if let Self::Java(java) = self {
java.try_enqueue_packet(packet);
}
}
pub fn try_enqueue_be_packet<P: BClientPacket>(&self, packet: &P) {
if let Self::Bedrock(bedrock) = self {
bedrock.try_enqueue_packet(packet);
} }
} }
pub fn try_enqueue_spawn_packet(&self, entity: &Arc<dyn crate::entity::EntityBase>) { pub fn try_enqueue_spawn_packet(&self, entity: &Arc<dyn crate::entity::EntityBase>) {
match self { 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()), Self::Bedrock(bedrock) => bedrock.enqueue_spawn_packet(entity.clone()),
} }
} }
@ -263,15 +276,10 @@ impl ClientPlatform {
} }
} }
pub async fn send_packet_now<P: ClientPacket>(&self, packet: &P) { pub async fn send_packet_now(&self, packet_data: Bytes) {
if let Self::Java(java) = self { match self {
java.send_packet_now(packet).await; Self::Java(java) => java.send_packet_now(packet_data).await,
} Self::Bedrock(bedrock) => bedrock.send_game_packet(packet_data).await,
}
pub async fn send_be_packet_now<P: BClientPacket>(&self, packet: &P) {
if let Self::Bedrock(bedrock) = self {
bedrock.send_game_packet(packet).await;
} }
} }
@ -281,16 +289,21 @@ impl ClientPlatform {
be_packet: &B, be_packet: &B,
) { ) {
match self { match self {
Self::Java(java) => java.send_packet_now(je_packet).await, Self::Java(java) => {
Self::Bedrock(bedrock) => bedrock.send_game_packet(be_packet).await, 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) { pub async fn send_packet_now_data(&self, data: Bytes) {
match self { self.send_packet_now(data).await;
Self::Java(java) => java.send_packet_now_data(data).await,
Self::Bedrock(bedrock) => bedrock.enqueue_packet_data(data).await,
}
} }
pub async fn kick(&self, reason: DisconnectReason, message: TextComponent) { pub async fn kick(&self, reason: DisconnectReason, message: TextComponent) {

View File

@ -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<LimiterState>,
}
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());
}
}
}

View File

@ -1113,7 +1113,7 @@ impl pumpkin::plugin::player::HostPlayer for PluginHostState {
// Sync to client // Sync to client
let stack_serializer = ItemStackSerializer::from(stack); let stack_serializer = ItemStackSerializer::from(stack);
let packet = CSetContainerSlot::new(0, 0, slot as i16, &stack_serializer); let packet = CSetContainerSlot::new(0, 0, slot as i16, &stack_serializer);
player.client.enqueue_packet(&packet).await; player.send_client_packet(&packet).await;
Ok(()) Ok(())
} }
@ -1139,7 +1139,7 @@ impl pumpkin::plugin::player::HostPlayer for PluginHostState {
// Sync to client // Sync to client
let stack_serializer = ItemStackSerializer::from(stack); let stack_serializer = ItemStackSerializer::from(stack);
let packet = CSetContainerSlot::new(0, 0, slot as i16, &stack_serializer); let packet = CSetContainerSlot::new(0, 0, slot as i16, &stack_serializer);
player.client.enqueue_packet(&packet).await; player.send_client_packet(&packet).await;
Ok(()) Ok(())
} }
@ -1204,7 +1204,7 @@ impl pumpkin::plugin::player::HostPlayer for PluginHostState {
let sync_id = handler.sync_id(); let sync_id = handler.sync_id();
let stack_serializer = ItemStackSerializer::from(stack); let stack_serializer = ItemStackSerializer::from(stack);
let packet = CSetContainerSlot::new(sync_id as i8, 0, slot as i16, &stack_serializer); 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(()) Ok(())
@ -1229,7 +1229,7 @@ impl pumpkin::plugin::player::HostPlayer for PluginHostState {
for slot in 0..27 { for slot in 0..27 {
let packet = let packet =
CSetContainerSlot::new(sync_id as i8, 0, slot as i16, &empty_serializer); 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)?; let player = player_from_resource(self, &player)?;
if let crate::net::ClientPlatform::Java(client) = player.client.as_ref() { if let crate::net::ClientPlatform::Java(client) = player.client.as_ref() {
client client
.send_packet_now(&pumpkin_protocol::java::client::play::CTransfer::new( .send_packet(&pumpkin_protocol::java::client::play::CTransfer::new(
&host, &host,
pumpkin_protocol::codec::var_int::VarInt(i32::from(port)), 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() { if let crate::net::ClientPlatform::Java(_) = player.client.as_ref() {
player player
.client .send_client_packet(&pumpkin_protocol::java::client::play::CCustomPayload::new(
.send_packet_now(&pumpkin_protocol::java::client::play::CCustomPayload::new(
&channel, &data, &channel, &data,
)) ))
.await; .await;
@ -3098,7 +3097,7 @@ impl pumpkin::plugin::player::HostJavaPlayer for PluginHostState {
match client.connection_state.load() { match client.connection_state.load() {
pumpkin_protocol::ConnectionState::Config => { pumpkin_protocol::ConnectionState::Config => {
client client
.send_packet_now( .send_packet(
&pumpkin_protocol::java::client::config::CConfigShowDialog::new( &pumpkin_protocol::java::client::config::CConfigShowDialog::new(
pumpkin_protocol::IdOr::Value(DialogNBT::from_dialog( pumpkin_protocol::IdOr::Value(DialogNBT::from_dialog(
&protocol_dialog, &protocol_dialog,
@ -3109,13 +3108,9 @@ impl pumpkin::plugin::player::HostJavaPlayer for PluginHostState {
} }
pumpkin_protocol::ConnectionState::Play => { pumpkin_protocol::ConnectionState::Play => {
client client
.send_packet_now( .send_packet(&pumpkin_protocol::java::client::play::CPlayShowDialog::new(
&pumpkin_protocol::java::client::play::CPlayShowDialog::new( pumpkin_protocol::IdOr::Value(DialogNBT::from_dialog(&protocol_dialog)),
pumpkin_protocol::IdOr::Value(DialogNBT::from_dialog( ))
&protocol_dialog,
)),
),
)
.await; .await;
} }
_ => {} _ => {}
@ -3142,16 +3137,14 @@ impl pumpkin::plugin::player::HostJavaPlayer for PluginHostState {
match client.connection_state.load() { match client.connection_state.load() {
pumpkin_protocol::ConnectionState::Config => { pumpkin_protocol::ConnectionState::Config => {
client client
.send_packet_now( .send_packet(
&pumpkin_protocol::java::client::config::CConfigClearDialog::new(), &pumpkin_protocol::java::client::config::CConfigClearDialog::new(),
) )
.await; .await;
} }
pumpkin_protocol::ConnectionState::Play => { pumpkin_protocol::ConnectionState::Play => {
client client
.send_packet_now( .send_packet(&pumpkin_protocol::java::client::play::CPlayClearDialog::new())
&pumpkin_protocol::java::client::play::CPlayClearDialog::new(),
)
.await; .await;
} }
_ => {} _ => {}
@ -3216,7 +3209,7 @@ impl pumpkin::plugin::player::HostJavaPlayer for PluginHostState {
if let crate::net::ClientPlatform::Java(client) = player.client.as_ref() { if let crate::net::ClientPlatform::Java(client) = player.client.as_ref() {
client client
.send_packet_now( .send_packet(
&pumpkin_protocol::java::client::play::CAddResourcePack::new( &pumpkin_protocol::java::client::play::CAddResourcePack::new(
&uuid, &uuid,
&pack.url, &pack.url,
@ -3248,7 +3241,7 @@ impl pumpkin::plugin::player::HostJavaPlayer for PluginHostState {
if let crate::net::ClientPlatform::Java(client) = player.client.as_ref() { if let crate::net::ClientPlatform::Java(client) = player.client.as_ref() {
client client
.send_packet_now( .send_packet(
&pumpkin_protocol::java::client::play::CRemoveResourcePack::new(Some(&uuid)), &pumpkin_protocol::java::client::play::CRemoveResourcePack::new(Some(&uuid)),
) )
.await; .await;
@ -3271,9 +3264,7 @@ impl pumpkin::plugin::player::HostJavaPlayer for PluginHostState {
if let crate::net::ClientPlatform::Java(client) = player.client.as_ref() { if let crate::net::ClientPlatform::Java(client) = player.client.as_ref() {
client client
.send_packet_now( .send_packet(&pumpkin_protocol::java::client::play::CRemoveResourcePack::new(None))
&pumpkin_protocol::java::client::play::CRemoveResourcePack::new(None),
)
.await; .await;
} }
Ok(()) Ok(())
@ -3554,7 +3545,7 @@ impl pumpkin::plugin::player::HostBedrockPlayer for PluginHostState {
}; };
if let crate::net::ClientPlatform::Bedrock(client) = player.client.as_ref() { if let crate::net::ClientPlatform::Bedrock(client) = player.client.as_ref() {
client.send_game_packet(&packet).await; client.send_packet(&packet).await;
} }
Ok(()) Ok(())
@ -3658,7 +3649,7 @@ impl pumpkin::plugin::player::HostBedrockPlayer for PluginHostState {
}; };
client client
.send_game_packet(&CModalFormRequest { .send_packet(&CModalFormRequest {
form_id: pumpkin_protocol::codec::var_uint::VarUInt(form_id), form_id: pumpkin_protocol::codec::var_uint::VarUInt(form_id),
form_data: form_json.to_string(), form_data: form_json.to_string(),
}) })
@ -3750,7 +3741,7 @@ impl pumpkin::plugin::player::HostBedrockPlayer for PluginHostState {
resource_packs: entries, resource_packs: entries,
}; };
client.send_game_packet(&packs_info).await; client.send_packet(&packs_info).await;
} }
Ok(()) Ok(())
} }

View File

@ -741,8 +741,8 @@ impl Server {
for player in world.players.load().iter() { for player in world.players.load().iter() {
*player.tab_list_header.lock().await = header.clone(); *player.tab_list_header.lock().await = header.clone();
*player.tab_list_footer.lock().await = footer.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), pumpkin_protocol::codec::var_int::VarInt(0),
); );
for world in self.worlds.load().iter() { for world in self.worlds.load().iter() {
for player in world.players.load().iter() { let players = world.players.load();
if player.subscribed_debug_sample.load(Ordering::Relaxed) let recipients = players
.iter()
.filter(|player| {
player.subscribed_debug_sample.load(Ordering::Relaxed)
&& player.permission_lvl.load() >= pumpkin_util::PermissionLvl::Two && player.permission_lvl.load() >= pumpkin_util::PermissionLvl::Two
{ })
player.client.try_enqueue_packet(&packet); .filter_map(|player| player.client.java());
} World::broadcast_java_clients(&packet, recipients);
}
} }
} }

View File

@ -211,12 +211,10 @@ impl ServerTickRateManager {
pub async fn update_joining_player(&self, player: &Player) { pub async fn update_joining_player(&self, player: &Player) {
if player.client.java_version() >= pumpkin_util::version::JavaMinecraftVersion::V_1_20_3 { if player.client.java_version() >= pumpkin_util::version::JavaMinecraftVersion::V_1_20_3 {
player player
.client .send_client_packet(&CTickingState::new(self.tickrate(), self.is_frozen()))
.send_packet_now(&CTickingState::new(self.tickrate(), self.is_frozen()))
.await; .await;
player player
.client .send_client_packet(&CTickingStep::new(
.send_packet_now(&CTickingStep::new(
self.frozen_ticks_to_run.load(Ordering::Relaxed).into(), self.frozen_ticks_to_run.load(Ordering::Relaxed).into(),
)) ))
.await; .await;

View File

@ -62,6 +62,9 @@ impl Ticker {
next_tick += tick_interval; 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! { tokio::select! {
() = sleep_until(next_tick) => {}, () = sleep_until(next_tick) => {},
() = STOP_INTERRUPT.cancelled() => { () = 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(); let now = Instant::now();
if now.saturating_duration_since(next_tick) > Duration::from_secs(5) { if now > next_tick {
next_tick = now; next_tick = now;
} }
} }

View File

@ -46,7 +46,7 @@ impl Worldborder {
pub async fn init_client(&self, client: &JavaClient) { pub async fn init_client(&self, client: &JavaClient) {
client client
.enqueue_packet(&CInitializeWorldBorder::new( .enqueue_client_packet(&CInitializeWorldBorder::new(
self.center_x, self.center_x,
self.center_z, self.center_z,
self.old_diameter, self.old_diameter,

View File

@ -105,7 +105,7 @@ impl Player {
}; };
let packet = CBossEvent::new(&bossbar.uuid, boss_action); let packet = CBossEvent::new(&bossbar.uuid, boss_action);
java.enqueue_packet(&packet).await; java.enqueue_client_packet(&packet).await;
} }
ClientPlatform::Bedrock(bedrock) => { ClientPlatform::Bedrock(bedrock) => {
let packet = BBossEvent { let packet = BBossEvent {
@ -117,7 +117,7 @@ impl Player {
overlay: bossbar.division.to_bedrock(), 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 boss_action = BosseventAction::Remove;
let packet = CBossEvent::new(&uuid, boss_action); let packet = CBossEvent::new(&uuid, boss_action);
java.enqueue_packet(&packet).await; java.enqueue_client_packet(&packet).await;
} }
ClientPlatform::Bedrock(bedrock) => { ClientPlatform::Bedrock(bedrock) => {
let packet = BBossEvent { let packet = BBossEvent {
boss_entity_id: VarLong(uuid.as_u128() as i64), boss_entity_id: VarLong(uuid.as_u128() as i64),
action: BossEventAction::Remove, 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 boss_action = BosseventAction::UpdateHealth(health);
let packet = CBossEvent::new(uuid, boss_action); let packet = CBossEvent::new(uuid, boss_action);
java.enqueue_packet(&packet).await; java.enqueue_client_packet(&packet).await;
} }
ClientPlatform::Bedrock(bedrock) => { ClientPlatform::Bedrock(bedrock) => {
let packet = BBossEvent { let packet = BBossEvent {
boss_entity_id: VarLong(uuid.as_u128() as i64), boss_entity_id: VarLong(uuid.as_u128() as i64),
action: BossEventAction::UpdateHealth(health), 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 boss_action = BosseventAction::UpdateTile(title);
let packet = CBossEvent::new(uuid, boss_action); let packet = CBossEvent::new(uuid, boss_action);
java.enqueue_packet(&packet).await; java.enqueue_client_packet(&packet).await;
} }
ClientPlatform::Bedrock(bedrock) => { ClientPlatform::Bedrock(bedrock) => {
let packet = BBossEvent { let packet = BBossEvent {
boss_entity_id: VarLong(uuid.as_u128() as i64), boss_entity_id: VarLong(uuid.as_u128() as i64),
action: BossEventAction::UpdateTitle(title.get_text()), 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); let packet = CBossEvent::new(uuid, boss_action);
java.enqueue_packet(&packet).await; java.enqueue_client_packet(&packet).await;
} }
ClientPlatform::Bedrock(bedrock) => { ClientPlatform::Bedrock(bedrock) => {
let packet = BBossEvent { let packet = BBossEvent {
@ -201,7 +201,7 @@ impl Player {
overlay: dividers.to_bedrock(), 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 boss_action = BosseventAction::UpdateFlags(flags.bits());
let packet = CBossEvent::new(uuid, boss_action); let packet = CBossEvent::new(uuid, boss_action);
java.enqueue_packet(&packet).await; java.enqueue_client_packet(&packet).await;
} }
ClientPlatform::Bedrock(bedrock) => { ClientPlatform::Bedrock(bedrock) => {
// For Bedrock, flags are part of properties (screen darken) // For Bedrock, flags are part of properties (screen darken)
@ -227,7 +227,7 @@ impl Player {
overlay: VarInt(0), overlay: VarInt(0),
}, },
}; };
bedrock.send_game_packet(&packet).await; bedrock.send_packet(&packet).await;
} }
} }
} }

View File

@ -60,7 +60,7 @@ pub async fn update_position(player: &Arc<Player>) {
match player.client.as_ref() { match player.client.as_ref() {
ClientPlatform::Java(java_client) => { ClientPlatform::Java(java_client) => {
java_client java_client
.send_packet_now(&CCenterChunk { .send_packet(&CCenterChunk {
chunk_x: new_chunk_center.x.into(), chunk_x: new_chunk_center.x.into(),
chunk_z: new_chunk_center.y.into(), chunk_z: new_chunk_center.y.into(),
}) })
@ -68,7 +68,7 @@ pub async fn update_position(player: &Arc<Player>) {
} }
ClientPlatform::Bedrock(bedrock_client) => { ClientPlatform::Bedrock(bedrock_client) => {
bedrock_client bedrock_client
.send_game_packet(&CNetworkChunkPublisherUpdate::new( .send_packet(&CNetworkChunkPublisherUpdate::new(
player.get_entity().block_pos.load(), player.get_entity().block_pos.load(),
u32::from(view_distance.get()) * 16, u32::from(view_distance.get()) * 16,
)) ))
@ -100,7 +100,7 @@ pub async fn update_position(player: &Arc<Player>) {
if let ClientPlatform::Java(client) = player.client.as_ref() { if let ClientPlatform::Java(client) = player.client.as_ref() {
for chunk in &unloading_chunks { for chunk in &unloading_chunks {
client client
.enqueue_packet(&CUnloadChunk::new(chunk.x, chunk.y)) .enqueue_client_packet(&CUnloadChunk::new(chunk.x, chunk.y))
.await; .await;
} }
} }

View File

@ -40,7 +40,7 @@ use crate::{
command::client_suggestions, command::client_suggestions,
entity::{Entity, EntityBase, NBTStorage, RemovalReason, player::Player, r#type::from_type}, entity::{Entity, EntityBase, NBTStorage, RemovalReason, player::Player, r#type::from_type},
error::PumpkinError, error::PumpkinError,
net::{ClientPlatform, java::JavaClient}, net::{ClientPlatform, bedrock::BedrockClient, java::JavaClient},
plugin::{ plugin::{
block::block_break::BlockBreakEvent, block::block_break::BlockBreakEvent,
player::{ player::{
@ -52,7 +52,7 @@ use crate::{
}; };
use arc_swap::ArcSwap; use arc_swap::ArcSwap;
use border::Worldborder; use border::Worldborder;
use bytes::BufMut; use bytes::{BufMut, Bytes};
use explosion::Explosion; use explosion::Explosion;
use pumpkin_config::BasicConfiguration; use pumpkin_config::BasicConfiguration;
use pumpkin_data::block_properties::{blocks_movement, is_air}; 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<Item = &'a Arc<Player>>, players: impl Iterator<Item = &'a Arc<Player>>,
) -> BTreeMap<JavaMinecraftVersion, Vec<&'a JavaClient>> { ) -> BTreeMap<JavaMinecraftVersion, Vec<&'a JavaClient>> {
let mut recipients_by_version: BTreeMap<JavaMinecraftVersion, Vec<&'a JavaClient>> = let mut recipients_by_version: BTreeMap<JavaMinecraftVersion, Vec<&'a JavaClient>> =
@ -677,6 +677,21 @@ impl World {
recipients_by_version recipients_by_version
} }
pub fn broadcast_java_clients<'a, P: ClientPacket>(
packet: &P,
recipients: impl Iterator<Item = &'a JavaClient>,
) {
let mut recipients_by_version: BTreeMap<JavaMinecraftVersion, Vec<&JavaClient>> =
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<P: ClientPacket>( fn broadcast_java_grouped<P: ClientPacket>(
packet: &P, packet: &P,
recipients_by_version: BTreeMap<JavaMinecraftVersion, Vec<&JavaClient>>, recipients_by_version: BTreeMap<JavaMinecraftVersion, Vec<&JavaClient>>,
@ -696,11 +711,55 @@ impl World {
}; };
for recipient in recipients { 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<Item = &'a Arc<BedrockClient>>,
) {
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::<P>(),
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<Item = &'a Arc<BedrockClient>>,
) {
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::<P>(),
err
);
return;
}
};
for recipient in recipients {
recipient.enqueue_packet(packet_data.clone()).await;
}
}
/// Broadcasts a packet to all connected players within the world. /// Broadcasts a packet to all connected players within the world.
/// Please avoid this as we want to replace it with `broadcast_editioned` /// Please avoid this as we want to replace it with `broadcast_editioned`
/// ///
@ -715,20 +774,8 @@ impl World {
pub fn broadcast_packet_all_sync<P: ClientPacket>(&self, packet: &P) { pub fn broadcast_packet_all_sync<P: ClientPacket>(&self, packet: &P) {
let players = self.players.load(); let players = self.players.load();
for player in players.iter() { let recipients_by_version = Self::collect_java_recipients_by_version(players.iter());
match player.client.as_ref() { Self::broadcast_java_grouped(packet, recipients_by_version);
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
}
}
}
} }
pub async fn broadcast_system_message(&self, message: &TextComponent, overlay: bool) { pub async fn broadcast_system_message(&self, message: &TextComponent, overlay: bool) {
@ -781,19 +828,16 @@ impl World {
) { ) {
let players = self.players.load(); let players = self.players.load();
let je_recipients_by_version = Self::collect_java_recipients_by_version(players.iter()); 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); Self::broadcast_java_grouped(je_packet, je_recipients_by_version);
Self::broadcast_bedrock_grouped_async(
for recipient in be_recipients { be_packet,
recipient.enqueue_packet(be_packet).await; 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( 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 TextComponent::empty(), // Not needed since we're injecting the name in the message for custom formatting
None, 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 { if let Some(signature) = chat_message.signature {
recipient recipient
@ -857,6 +912,7 @@ impl World {
) { ) {
let players = self.players.load(); let players = self.players.load();
let mut java_recipients = Vec::new(); let mut java_recipients = Vec::new();
let mut bedrock_recipients = Vec::new();
for p in players.iter() { for p in players.iter() {
if except.contains(&p.gameprofile.id) { if except.contains(&p.gameprofile.id) {
@ -864,13 +920,14 @@ impl World {
} }
match p.client.as_ref() { match p.client.as_ref() {
ClientPlatform::Java(_) => java_recipients.push(p), 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 = let recipients_by_version =
Self::collect_java_recipients_by_version(java_recipients.into_iter()); Self::collect_java_recipients_by_version(java_recipients.into_iter());
Self::broadcast_java_grouped(je_packet, recipients_by_version); 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 /// Broadcasts the skin layers of a player, encoding the metadata for each Java client's own
@ -882,13 +939,24 @@ impl World {
skin_parts: u8, skin_parts: u8,
be_packet: &B, 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) { if except.contains(&p.gameprofile.id) {
continue; continue;
} }
match p.client.as_ref() { match p.client.as_ref() {
ClientPlatform::Java(client) => { ClientPlatform::Java(_) => java_recipients.push(p),
let version = client.version.load(); 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 { if version < CURRENT_MC_VERSION {
continue; continue;
} }
@ -906,14 +974,17 @@ impl World {
let _ = meta.write(&mut buf, &version); let _ = meta.write(&mut buf, &version);
} }
buf.put_u8(255); buf.put_u8(255);
client let packet = CSetEntityMetadata::new(entity_id.into(), buf.into());
.try_enqueue_packet(&CSetEntityMetadata::new(entity_id.into(), buf.into())); if let Ok(packet_data) = JavaClient::serialize_packet_for_version(&packet, version) {
} for recipient in recipients {
ClientPlatform::Bedrock(be_client) => be_client.try_enqueue_packet(be_packet), recipient.try_enqueue_packet(packet_data.clone());
} }
} }
} }
Self::broadcast_bedrock_grouped(be_packet, bedrock_recipients.into_iter());
}
pub async fn broadcast_packet_except_editioned<J: ClientPacket, B: BClientPacket>( pub async fn broadcast_packet_except_editioned<J: ClientPacket, B: BClientPacket>(
&self, &self,
except: &[uuid::Uuid], except: &[uuid::Uuid],
@ -930,17 +1001,14 @@ impl World {
} }
match p.client.as_ref() { match p.client.as_ref() {
ClientPlatform::Java(_) => java_recipients.push(p), 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 = let recipients_by_version =
Self::collect_java_recipients_by_version(java_recipients.into_iter()); Self::collect_java_recipients_by_version(java_recipients.into_iter());
Self::broadcast_java_grouped(je_packet, recipients_by_version); Self::broadcast_java_grouped(je_packet, recipients_by_version);
Self::broadcast_bedrock_grouped_async(be_packet, bedrock_recipients.into_iter()).await;
for be_client in bedrock_recipients {
be_client.enqueue_packet(be_packet).await;
}
} }
/// Broadcasts a packet to all connected players within the world, excluding the specified players. /// Broadcasts a packet to all connected players within the world, excluding the specified players.
@ -1025,11 +1093,13 @@ impl World {
}; };
let chunk_pos = BlockPos::floored_v(*position).chunk_position(); let chunk_pos = BlockPos::floored_v(*position).chunk_position();
if let Ok(data) = pumpkin_protocol::bedrock::packet_encoder::serialize_packet(&packet) {
for player in self.players.load().iter() { for player in self.players.load().iter() {
if is_within_view_distance(chunk_pos, player.get_entity().chunk_pos.load(), 1) if is_within_view_distance(chunk_pos, player.get_entity().chunk_pos.load(), 1)
&& let ClientPlatform::Bedrock(client) = player.client.as_ref() && let ClientPlatform::Bedrock(client) = player.client.as_ref()
{ {
client.try_enqueue_packet(&packet); client.try_enqueue_packet(data.clone());
}
} }
} }
} }
@ -1149,7 +1219,11 @@ impl World {
let entity = player.get_entity(); let entity = player.get_entity();
let pos = entity.pos.load(); let pos = entity.pos.load();
let bb = entity.bounding_box.load().expand(1.0, 0.5, 1.0); 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::<Vec<_>>(), .collect::<Vec<_>>(),
); );
@ -1218,8 +1292,10 @@ impl World {
let entity_pos = entity_inner.pos.load(); let entity_pos = entity_inner.pos.load();
let entity_bb = entity_inner.bounding_box.load(); let entity_bb = entity_inner.bounding_box.load();
for (player, player_pos, player_bb) in p_cache.iter() { for (player, player_pos, player_bb, player_chunk) in p_cache.iter() {
if (player_pos.x - entity_pos.x).abs() < 5.0 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.y - entity_pos.y).abs() < 5.0
&& (player_pos.z - entity_pos.z).abs() < 5.0 && (player_pos.z - entity_pos.z).abs() < 5.0
&& player_bb.intersects(&entity_bb) && player_bb.intersects(&entity_bb)
@ -1251,11 +1327,13 @@ impl World {
let block_entity_future = async move { let block_entity_future = async move {
let t = tokio::time::Instant::now(); let t = tokio::time::Instant::now();
let mut tasks = tokio::task::JoinSet::new(); let mut tasks = tokio::task::JoinSet::new();
for be in block_entities { for be_batch in block_entities.chunks(16) {
let be_clone = be.clone(); let batch = be_batch.to_vec();
let w_clone = world_for_be.clone(); let w_clone = world_for_be.clone();
tasks.spawn(async move { 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 { 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) { pub async fn flush_block_updates(&self) {
let mut block_state_updates_by_chunk_section: HashMap< let mut block_state_updates_by_chunk_section: HashMap<
Vector3<i32>, Vector3<i32>,
@ -1367,27 +1446,44 @@ impl World {
is_within_view_distance(chunk_pos, center, view_distance) is_within_view_distance(chunk_pos, center, view_distance)
}); });
let mut bedrock_packets: Vec<Bytes> = 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 { for p in recipients {
match p.client.as_ref() { match p.client.as_ref() {
ClientPlatform::Java(_) => java_recipients.push(p), ClientPlatform::Java(_) => java_recipients.push(p),
ClientPlatform::Bedrock(be_client) => { ClientPlatform::Bedrock(be_client) => {
for (block_pos, block_state_id) in &updates { bedrock_recipients.push(be_client);
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,
));
} }
} }
} }
for be_client in bedrock_recipients {
for packet_data in &bedrock_packets {
be_client.try_enqueue_packet(packet_data.clone());
} }
} }
@ -1399,6 +1495,22 @@ impl World {
); );
} }
let mut bedrock_water_packets: Vec<Bytes> = 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 players = self.players.load();
let recipients = players.iter().filter(|player| { let recipients = players.iter().filter(|player| {
let center = player.get_entity().chunk_pos.load(); let center = player.get_entity().chunk_pos.load();
@ -1406,18 +1518,11 @@ impl World {
is_within_view_distance(chunk_pos, center, view_distance) is_within_view_distance(chunk_pos, center, view_distance)
}); });
for player in recipients { for player in recipients {
let ClientPlatform::Bedrock(client) = player.client.as_ref() else { if let ClientPlatform::Bedrock(client) = player.client.as_ref() {
continue; for packet_data in &bedrock_water_packets {
}; client.try_enqueue_packet(packet_data.clone());
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,
),
);
} }
} }
} }
@ -1486,6 +1591,8 @@ impl World {
#[expect(clippy::too_many_lines)] #[expect(clippy::too_many_lines)]
pub async fn tick_chunks(self: &Arc<Self>) { pub async fn tick_chunks(self: &Arc<Self>) {
const BATCH_SIZE: usize = 32;
let active_chunks = self.active_chunks.load(); let active_chunks = self.active_chunks.load();
let tick_data = self.level.get_tick_data(&active_chunks); let tick_data = self.level.get_tick_data(&active_chunks);
@ -1493,10 +1600,12 @@ impl World {
let mut chunk_tasks = tokio::task::JoinSet::new(); let mut chunk_tasks = tokio::task::JoinSet::new();
// 1. Spawn Block Ticks // 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 world = self.clone();
let pos = scheduled_tick.position; // Clone for the move closure
chunk_tasks.spawn(async move { chunk_tasks.spawn(async move {
for scheduled_tick in batch {
let pos = scheduled_tick.position;
let block = world.get_block(&pos); let block = world.get_block(&pos);
if let Some(pumpkin_block) = world.block_registry.get_pumpkin_block(block.id) { if let Some(pumpkin_block) = world.block_registry.get_pumpkin_block(block.id) {
pumpkin_block pumpkin_block
@ -1507,30 +1616,34 @@ impl World {
}) })
.await; .await;
} }
}
}); });
} }
// 2. Spawn Fluid Ticks // 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 world = self.clone();
let pos = scheduled_tick.position;
chunk_tasks.spawn(async move { chunk_tasks.spawn(async move {
for scheduled_tick in batch {
let pos = scheduled_tick.position;
let fluid = world.get_fluid(&pos); let fluid = world.get_fluid(&pos);
if let Some(pumpkin_fluid) = world.block_registry.get_pumpkin_fluid(fluid.id) { if let Some(pumpkin_fluid) = world.block_registry.get_pumpkin_fluid(fluid.id) {
pumpkin_fluid.on_scheduled_tick(&world, fluid, &pos).await; pumpkin_fluid.on_scheduled_tick(&world, fluid, &pos).await;
} }
}
}); });
} }
// 3. Spawn Random Ticks // 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 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 { chunk_tasks.spawn(async move {
let (block, fluid) = match (tick_block, tick_fluid) { for scheduled_tick in batch {
let pos = scheduled_tick.position;
let (block, fluid) =
match (scheduled_tick.tick_block, scheduled_tick.tick_fluid) {
(true, true) => { (true, true) => {
let (b, f) = world.get_block_and_fluid(&pos); let (b, f) = world.get_block_and_fluid(&pos);
(Some(b), Some(f)) (Some(b), Some(f))
@ -1541,7 +1654,8 @@ impl World {
}; };
if let Some(block) = block if let Some(block) = block
&& let Some(pumpkin_block) = world.block_registry.get_pumpkin_block(block.id) && let Some(pumpkin_block) =
world.block_registry.get_pumpkin_block(block.id)
{ {
pumpkin_block pumpkin_block
.random_tick(RandomTickArgs { .random_tick(RandomTickArgs {
@ -1553,10 +1667,12 @@ impl World {
} }
if let Some(fluid) = fluid if let Some(fluid) = fluid
&& let Some(pumpkin_fluid) = world.block_registry.get_pumpkin_fluid(fluid.id) && let Some(pumpkin_fluid) =
world.block_registry.get_pumpkin_fluid(fluid.id)
{ {
pumpkin_fluid.random_tick(fluid, &world, &pos).await; pumpkin_fluid.random_tick(fluid, &world, &pos).await;
} }
}
}); });
} }
@ -1592,15 +1708,18 @@ impl World {
spawning_chunks.shuffle(&mut rng()); 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 world = self.clone();
let s_list = spawn_list.clone(); let s_list = spawn_list.clone();
let s_state = spawn_state.clone(); let s_state = spawn_state.clone();
chunk_tasks.spawn(async move { chunk_tasks.spawn(async move {
for (pos, chunk) in batch {
world world
.tick_spawning_chunk(pos, &chunk, &s_list, &s_state) .tick_spawning_chunk(pos, &chunk, &s_list, &s_state)
.await; .await;
}
}); });
} }
} }
@ -2257,8 +2376,7 @@ impl World {
return; return;
}; };
client let start_game = CStartGame {
.send_game_packet(&CStartGame {
entity_id: VarLong(runtime_id as _), entity_id: VarLong(runtime_id as _),
runtime_entity_id: VarULong(runtime_id), runtime_entity_id: VarULong(runtime_id),
player_gamemode: player.gamemode.load(), player_gamemode: player.gamemode.load(),
@ -2299,13 +2417,16 @@ impl World {
world_id: String::new(), world_id: String::new(),
owner_id: String::new(), owner_id: String::new(),
}, },
}) };
.await; 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 let item_registry = CItemRegistry {
.send_game_packet(&CItemRegistry {
items: BedrockItem::ALL_BEDROCK_ITEMS items: BedrockItem::ALL_BEDROCK_ITEMS
.iter() .iter()
.map(|b| ItemDefinition { .map(|b| ItemDefinition {
@ -2320,8 +2441,10 @@ impl World {
component_data: b.definition_components.into(), component_data: b.definition_components.into(),
}) })
.collect::<Vec<_>>(), .collect::<Vec<_>>(),
}) };
.await; 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, entries) = CREATIVE_CONTENT.get_or_init(|| {
let groups = pumpkin_data::bedrock_creative::CREATIVE_GROUPS let groups = pumpkin_data::bedrock_creative::CREATIVE_GROUPS
@ -2379,10 +2502,10 @@ impl World {
(groups, entries) (groups, entries)
}); });
let creative_content = CCreativeContent { groups, entries };
client if let Ok(data) = client.serialize_packet(&creative_content) {
.send_game_packet(&CCreativeContent { groups, entries }) client.send_game_packet(data).await;
.await; }
let bedrock_recipes = BEDROCK_CRAFTING_DATA.get_or_init(|| { let bedrock_recipes = BEDROCK_CRAFTING_DATA.get_or_init(|| {
use pumpkin_data::item::{Item, JavaToBedrockItemMapping}; use pumpkin_data::item::{Item, JavaToBedrockItemMapping};
@ -2556,12 +2679,13 @@ impl World {
mapped_recipes mapped_recipes
}); });
client let crafting_data = pumpkin_protocol::bedrock::client::CCraftingData {
.send_game_packet(&pumpkin_protocol::bedrock::client::CCraftingData {
recipes: bedrock_recipes.clone(), recipes: bedrock_recipes.clone(),
clean_recipes: false, clean_recipes: false,
}) };
.await; if let Ok(data) = client.serialize_packet(&crafting_data) {
client.send_game_packet(data).await;
}
player player
.on_screen_handler_opened(player.player_screen_handler.clone()) .on_screen_handler_opened(player.player_screen_handler.clone())
@ -2584,7 +2708,9 @@ impl World {
}, },
tick: VarULong(0), 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; player.send_abilities_update().await;
{ {
@ -2594,7 +2720,7 @@ impl World {
}; };
client client
.enqueue_packet_internal(&CUpdateAttributes { .enqueue_client_packet(&CUpdateAttributes {
runtime_id: VarULong(runtime_id), runtime_id: VarULong(runtime_id),
attributes: vec![ attributes: vec![
Attribute { Attribute {
@ -2808,7 +2934,7 @@ impl World {
}], }],
}; };
// Send PlayerList FIRST // Send PlayerList FIRST
client.send_game_packet(&ex_player_list).await; client.send_packet(&ex_player_list).await;
let ex_add_player = CAddPlayer { let ex_add_player = CAddPlayer {
uuid: ex_profile.id, uuid: ex_profile.id,
@ -2847,7 +2973,7 @@ impl World {
build_platform: BuildPlatform::Unknown, 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; let ex_held_item = existing_player.inventory().held_item().await;
@ -2859,7 +2985,7 @@ impl World {
0, 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); player.has_played_before.store(true, Ordering::Relaxed);
@ -2908,7 +3034,7 @@ impl World {
}; };
// Send the login packet for our new player // Send the login packet for our new player
client client
.send_packet_now(&CLogin::new( .send_packet(&CLogin::new(
entity_id, entity_id,
base_config.hardcore, base_config.hardcore,
&dimensions, &dimensions,
@ -3002,7 +3128,7 @@ impl World {
} }
} }
client client
.send_packet_now(&pumpkin_protocol::java::client::play::CUpdateTagsPlay::new( .send_packet(&pumpkin_protocol::java::client::play::CUpdateTagsPlay::new(
&tags, &tags,
)) ))
.await; .await;
@ -3049,11 +3175,11 @@ impl World {
} }
} }
if client.version.load() >= JavaMinecraftVersion::V_1_20_2 { 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 { 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(); let velocity = player.living_entity.entity.velocity.load();
@ -3199,7 +3325,7 @@ impl World {
debug!("Sending player info to {}", player.gameprofile.name); debug!("Sending player info to {}", player.gameprofile.name);
client client
.enqueue_packet(&CPlayerInfoUpdate::new(action_flags.bits(), &entries)) .enqueue_client_packet(&CPlayerInfoUpdate::new(action_flags.bits(), &entries))
.await; .await;
// Send tab_list_names for existing players with custom names // Send tab_list_names for existing players with custom names
@ -3211,7 +3337,7 @@ impl World {
actions: &actions, actions: &actions,
}]; }];
client client
.enqueue_packet(&CPlayerInfoUpdate::new( .enqueue_client_packet(&CPlayerInfoUpdate::new(
PlayerInfoFlags::UPDATE_DISPLAY_NAME.bits(), PlayerInfoFlags::UPDATE_DISPLAY_NAME.bits(),
&java_player, &java_player,
)) ))
@ -3440,7 +3566,7 @@ impl World {
// END // END
buf.put_u8(255); buf.put_u8(255);
client client
.enqueue_packet(&CSetEntityMetadata::new( .enqueue_client_packet(&CSetEntityMetadata::new(
existing_player.get_entity().entity_id.into(), existing_player.get_entity().entity_id.into(),
buf.into(), buf.into(),
)) ))
@ -3492,7 +3618,7 @@ impl World {
// Start waiting for level chunks. Sets the "Loading Terrain" screen // Start waiting for level chunks. Sets the "Loading Terrain" screen
debug!("Sending waiting chunks to {}", player.gameprofile.name); debug!("Sending waiting chunks to {}", player.gameprofile.name);
client client
.send_packet_now(&CGameEvent::new(GameEvent::StartWaitingChunks, 0.0)) .send_packet(&CGameEvent::new(GameEvent::StartWaitingChunks, 0.0))
.await; .await;
self.worldborder.lock().await.init_client(client).await; self.worldborder.lock().await.init_client(client).await;
@ -3517,7 +3643,7 @@ impl World {
}; };
client client
.send_packet_now(&CPlayerSpawnPosition::new( .send_packet(&CPlayerSpawnPosition::new(
spawn_block_pos, spawn_block_pos,
yaw, yaw,
pitch, pitch,
@ -3529,7 +3655,7 @@ impl World {
let weather = self.weather.lock().await; let weather = self.weather.lock().await;
if weather.raining { if weather.raining {
client client
.enqueue_packet(&CGameEvent::new(GameEvent::BeginRaining, 0.0)) .enqueue_client_packet(&CGameEvent::new(GameEvent::BeginRaining, 0.0))
.await; .await;
// Calculate rain and thunder levels directly from public fields // Calculate rain and thunder levels directly from public fields
@ -3538,10 +3664,10 @@ impl World {
drop(weather); drop(weather);
client client
.enqueue_packet(&CGameEvent::new(GameEvent::RainLevelChange, rain_level)) .enqueue_client_packet(&CGameEvent::new(GameEvent::RainLevelChange, rain_level))
.await; .await;
client client
.enqueue_packet(&CGameEvent::new( .enqueue_client_packet(&CGameEvent::new(
GameEvent::ThunderLevelChange, GameEvent::ThunderLevelChange,
thunder_level, thunder_level,
)) ))
@ -3566,13 +3692,15 @@ impl World {
if let crate::net::ClientPlatform::Java(java_client) = player.client.as_ref() if let crate::net::ClientPlatform::Java(java_client) = player.client.as_ref()
&& server.advanced_config.recipe.send_recipes && server.advanced_config.recipe.send_recipes
{ {
java_client let settings_packet = CRecipeBookSettings::default_closed();
.send_packet_now(&CRecipeBookSettings::default_closed()) if let Ok(data) = java_client.serialize_packet(&settings_packet) {
.await; java_client.send_packet_now(data).await;
}
let dynamic_recipes = server.recipe_manager.get_dynamic_recipes().await; let dynamic_recipes = server.recipe_manager.get_dynamic_recipes().await;
java_client let add_packet = CRecipeBookAdd::new(true, &dynamic_recipes);
.send_packet_now(&CRecipeBookAdd::new(true, &dynamic_recipes)) if let Ok(data) = java_client.serialize_packet(&add_packet) {
.await; java_client.send_packet_now(data).await;
}
} }
let msg_comp = TextComponent::translate_cross( let msg_comp = TextComponent::translate_cross(
@ -3640,8 +3768,7 @@ impl World {
// TODO: World spawn (compass stuff) // TODO: World spawn (compass stuff)
player player
.client .send_client_packet(&CGameEvent::new(GameEvent::StartWaitingChunks, 0.0))
.enqueue_packet(&CGameEvent::new(GameEvent::StartWaitingChunks, 0.0))
.await; .await;
let entity = &player.get_entity(); let entity = &player.get_entity();
@ -3712,8 +3839,7 @@ impl World {
continue; continue;
} }
player player
.client .send_client_packet(&CExplosion::new(
.enqueue_packet(&CExplosion::new(
position, position,
power, power,
block_count as i32, block_count as i32,
@ -3761,8 +3887,7 @@ impl World {
} else { } else {
// No valid respawn point - send notification and use world spawn // No valid respawn point - send notification and use world spawn
player player
.client .send_client_packet(&CGameEvent::new(GameEvent::NoRespawnBlockAvailable, 0.0))
.send_packet_now(&CGameEvent::new(GameEvent::NoRespawnBlockAvailable, 0.0))
.await; .await;
// FIXME: This spawn position calculation is incorrect. Should use vanilla's // 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) // Send respawn packet with target dimension (using send_packet_now to ensure proper order)
player player
.client .send_client_packet(&CRespawn::new(
.send_packet_now(&CRespawn::new(
PlayerSpawnData::new( PlayerSpawnData::new(
target_world.dimension.clone(), target_world.dimension.clone(),
biome::hash_seed(target_world.level.seed.0), biome::hash_seed(target_world.level.seed.0),
@ -3950,7 +4074,6 @@ impl World {
_ => 0, _ => 0,
}; };
player player
.client
.send_packet_now_editioned( .send_packet_now_editioned(
&CPlayerSpawnPosition::new( &CPlayerSpawnPosition::new(
spawn_block_pos, spawn_block_pos,
@ -3999,13 +4122,11 @@ impl World {
.get_or_fetch_chunk(center_chunk, std::clone::Clone::clone) .get_or_fetch_chunk(center_chunk, std::clone::Clone::clone)
.await; .await;
if java_client.version.load() >= JavaMinecraftVersion::V_1_20_2 { 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 { if java_client.version.load() >= JavaMinecraftVersion::V_1_20_2 {
java_client java_client.send_packet(&CChunkBatchEnd::new(1u16)).await;
.send_packet_now(&CChunkBatchEnd::new(1u16))
.await;
} }
} }
@ -4756,7 +4877,7 @@ impl World {
if let Some(player) = self.get_player_by_uuid(from.entity_uuid) if let Some(player) = self.get_player_by_uuid(from.entity_uuid)
&& let ClientPlatform::Bedrock(client) = player.client.as_ref() && 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( self.broadcast_to_chunk_except_editioned(
@ -5096,7 +5217,7 @@ impl World {
match &cause { match &cause {
Some(player) => { Some(player) => {
if let ClientPlatform::Bedrock(client) = player.client.as_ref() { 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( self.broadcast_to_chunk_except_editioned(
chunk_pos, chunk_pos,
@ -6045,15 +6166,17 @@ impl World {
packet: &P, packet: &P,
) { ) {
let players = self.players.load(); 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 center = player.get_entity().chunk_pos.load();
let view_distance = get_view_distance(player).get() as i32; let view_distance = get_view_distance(player).get() as i32;
is_within_view_distance(chunk_pos, center, view_distance) if is_within_view_distance(chunk_pos, center, view_distance)
}) { && let ClientPlatform::Bedrock(client) = player.client.as_ref()
if let ClientPlatform::Bedrock(client) = player.client.as_ref() { {
client.try_enqueue_packet(packet); return Some(client);
}
} }
None
});
Self::broadcast_bedrock_grouped(packet, recipients);
} }
pub fn broadcast_to_chunk_editioned_sync<J: ClientPacket, B: BClientPacket>( pub fn broadcast_to_chunk_editioned_sync<J: ClientPacket, B: BClientPacket>(
@ -6064,6 +6187,7 @@ impl World {
) { ) {
let players = self.players.load(); let players = self.players.load();
let mut java_recipients = Vec::new(); let mut java_recipients = Vec::new();
let mut bedrock_recipients = Vec::new();
let recipients = players.iter().filter(|p| { let recipients = players.iter().filter(|p| {
let center = p.get_entity().chunk_pos.load(); let center = p.get_entity().chunk_pos.load();
@ -6074,13 +6198,14 @@ impl World {
for p in recipients { for p in recipients {
match p.client.as_ref() { match p.client.as_ref() {
ClientPlatform::Java(_) => java_recipients.push(p), 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 = let recipients_by_version =
Self::collect_java_recipients_by_version(java_recipients.into_iter()); Self::collect_java_recipients_by_version(java_recipients.into_iter());
Self::broadcast_java_grouped(je_packet, recipients_by_version); 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. /// Broadcasts a packet to chunk watchers, excluding specific players.
@ -6130,17 +6255,14 @@ impl World {
for p in recipients { for p in recipients {
match p.client.as_ref() { match p.client.as_ref() {
ClientPlatform::Java(_) => java_recipients.push(p), 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 = let je_recipients_by_version =
Self::collect_java_recipients_by_version(java_recipients.into_iter()); Self::collect_java_recipients_by_version(java_recipients.into_iter());
Self::broadcast_java_grouped(je_packet, je_recipients_by_version); Self::broadcast_java_grouped(je_packet, je_recipients_by_version);
Self::broadcast_bedrock_grouped_async(be_packet, bedrock_recipients.into_iter()).await;
for recipient in bedrock_recipients {
recipient.enqueue_packet(be_packet).await;
}
} }
pub async fn emit_game_event(&self, event_key: impl Into<String>, position: Vector3<f64>) { pub async fn emit_game_event(&self, event_key: impl Into<String>, position: Vector3<f64>) {

View File

@ -137,7 +137,7 @@ impl PortalType {
match player.client.as_ref() { match player.client.as_ref() {
crate::net::ClientPlatform::Java(client) => { crate::net::ClientPlatform::Java(client) => {
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, pumpkin_protocol::java::client::play::GameEvent::WinGame,
1.0, 1.0,
)) ))
@ -145,7 +145,7 @@ impl PortalType {
} }
crate::net::ClientPlatform::Bedrock(client) => { crate::net::ClientPlatform::Bedrock(client) => {
client client
.send_game_packet( .send_packet(
&pumpkin_protocol::bedrock::client::CShowCredits::new( &pumpkin_protocol::bedrock::client::CShowCredits::new(
pumpkin_protocol::codec::var_ulong::VarULong( pumpkin_protocol::codec::var_ulong::VarULong(
caller.get_entity().entity_id as u64, caller.get_entity().entity_id as u64,