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

View File

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

View File

@ -1,4 +1,4 @@
use crate::{AuthenticationConfig, CompressionConfig};
use crate::{AuthenticationConfig, CompressionConfig, PacketLimiterConfig};
use serde::{Deserialize, Serialize};
use std::net::SocketAddr;
use std::num::NonZero;
@ -21,12 +21,21 @@ pub struct JavaConfig {
pub view_distance: NonZero<u8>,
/// The maximum simulated view distance.
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.
pub compression: CompressionConfig,
/// Message of the Day; the server's description displayed on the status screen.
pub motd: String,
/// Authentication settings for client connections.
pub authentication: AuthenticationConfig,
/// Packet rate limiting settings.
pub packet_limiter: PacketLimiterConfig,
}
impl Default for JavaConfig {
@ -44,9 +53,49 @@ impl Default for JavaConfig {
max_players: 1000,
view_distance,
simulation_distance,
keep_alive_time: 15,
compression: CompressionConfig::default(),
motd: "A blazingly fast Pumpkin server!".to_string(),
authentication: AuthenticationConfig::default(),
packet_limiter: PacketLimiterConfig::default(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_keep_alive_time() {
let config = JavaConfig::default();
assert_eq!(config.keep_alive_time, 15);
}
#[test]
fn keep_alive_time_deserialization() {
let toml_snake = r"
keep_alive_time = 20
";
let config: JavaConfig = toml::from_str(toml_snake).unwrap();
assert_eq!(config.keep_alive_time, 20);
let toml_kebab = r"
keep-alive-time = 25
";
let config: JavaConfig = toml::from_str(toml_kebab).unwrap();
assert_eq!(config.keep_alive_time, 25);
let toml_interval = r"
keep_alive_interval = 30
";
let config: JavaConfig = toml::from_str(toml_interval).unwrap();
assert_eq!(config.keep_alive_time, 30);
let toml_interval_kebab = r"
keep-alive-interval = 35
";
let config: JavaConfig = toml::from_str(toml_interval_kebab).unwrap();
assert_eq!(config.keep_alive_time, 35);
}
}

View File

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

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_9: 0i32,
v1_21_11: 0i32,
v26_1: -1i32,
v26_2: -1i32,
v26_1: 0i32,
v26_2: 0i32,
};
pub const LOGIN_COOKIE_RESPONSE: super::PacketId = super::PacketId {
v1_20_5: 4i32,

View File

@ -86,6 +86,44 @@ impl BedrockBatchEncoder {
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)]

View File

@ -27,3 +27,15 @@ impl ClientPacket for CConfigPing {
Ok(())
}
}
impl<'a> crate::ServerPacket<'a> for CConfigPing {
fn read(
read: &mut &'a [u8],
_version: &JavaMinecraftVersion,
) -> Result<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 {
fn read(
read: &mut &'a [u8],
_version: &JavaMinecraftVersion,
version: &JavaMinecraftVersion,
) -> Result<Self, crate::ser::ReadingError> {
Ok(Self {
teleport_id: read.get_var_int()?,
// TODO
position: Vector3::new(0.0, 0.0, 0.0),
delta: Vector3::new(0.0, 0.0, 0.0),
yaw: 0.0,
pitch: 0.0,
relatives: Vec::new(),
})
if version >= &JavaMinecraftVersion::V_1_21_2 {
let teleport_id = read.get_var_int()?;
let x = read.get_f64_be()?;
let y = read.get_f64_be()?;
let z = read.get_f64_be()?;
let dx = read.get_f64_be()?;
let dy = read.get_f64_be()?;
let dz = read.get_f64_be()?;
let yaw = read.get_f32_be()?;
let pitch = read.get_f32_be()?;
let relatives_bits = read.get_i32_be()?;
Ok(Self {
teleport_id,
position: Vector3::new(x, y, z),
delta: Vector3::new(dx, dy, dz),
yaw,
pitch,
relatives: PositionFlag::from_bitfield(relatives_bits),
})
} else {
let x = read.get_f64_be()?;
let y = read.get_f64_be()?;
let z = read.get_f64_be()?;
let yaw = read.get_f32_be()?;
let pitch = read.get_f32_be()?;
let relatives_bits = i32::from(read.get_u8()?);
let teleport_id = if version >= &JavaMinecraftVersion::V_1_9 {
read.get_var_int()?
} else {
VarInt(0)
};
if version >= &JavaMinecraftVersion::V_1_20_2 {
let _ = read.get_bool()?;
}
Ok(Self {
teleport_id,
position: Vector3::new(x, y, z),
delta: Vector3::new(0.0, 0.0, 0.0),
yaw,
pitch,
relatives: PositionFlag::from_bitfield(relatives_bits),
})
}
}
}

View File

@ -1,12 +1,14 @@
use aes::cipher::KeyIvInit;
use bytes::Bytes;
use flate2::{Compress, Compression, FlushCompress, Status};
use pumpkin_util::version::JavaMinecraftVersion;
use thiserror::Error;
use tokio::io::{AsyncWrite, AsyncWriteExt};
use crate::{
Aes128Cfb8Enc, CompressionLevel, CompressionThreshold, MAX_PACKET_DATA_SIZE, MAX_PACKET_SIZE,
PacketEncodeError, StreamEncryptor, VarInt,
Aes128Cfb8Enc, ClientPacket, CompressionLevel, CompressionThreshold, MAX_PACKET_DATA_SIZE,
MAX_PACKET_SIZE, PacketEncodeError, StreamEncryptor, VarInt, WritingError,
ser::NetworkWriteExt,
};
// raw -> compress -> encrypt
@ -318,6 +320,28 @@ impl<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)]
#[error("Invalid compression Level")]
pub struct CompressionLevelError;

View File

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

View File

@ -1282,6 +1282,12 @@ impl GenerationSchedule {
});
if node.stage > effective_target {
if let Some(holder) = self.chunk_map.get_mut(&node.pos) {
let task_slot = &mut holder.tasks[node.stage as usize];
if *task_slot == task.1 {
*task_slot = NodeKey::null();
}
}
self.waiting_for_chunks.remove(&task.1);
self.drop_node(task.1);
continue;

View File

@ -416,3 +416,47 @@ fn dag_prune_edge_chain_removes_dead_target_edges() {
assert!(head.is_null());
assert_eq!(graph.edges.len(), 0);
}
#[test]
fn cancelled_out_of_range_task_clears_holder_task_slot() {
let mut graph = DAG::default();
let mut chunk_map = HashMap::new();
let pos = ChunkPos::new(0, 0);
let stage = StagedChunkEnum::Biomes;
let node_key = graph.nodes.insert(Node::new(pos, stage));
let mut holder = ChunkHolder {
current_stage: StagedChunkEnum::Empty,
target_stage: StagedChunkEnum::None, // Chunk was downgraded / moved out of range
dependency_stage: StagedChunkEnum::None,
..Default::default()
};
holder.tasks[stage as usize] = node_key;
chunk_map.insert(pos, holder);
// Simulate task processing loop cancellation when node.stage > effective_target
let node = graph.nodes.get(node_key).unwrap().clone();
let effective_target = chunk_map.get(&node.pos).map_or(StagedChunkEnum::None, |h| {
h.target_stage.max(h.dependency_stage)
});
assert!(node.stage > effective_target);
if let Some(holder) = chunk_map.get_mut(&node.pos) {
let task_slot = &mut holder.tasks[node.stage as usize];
if *task_slot == node_key {
*task_slot = NodeKey::null();
}
}
graph.fast_drop_node(node_key);
let holder = chunk_map.get(&pos).unwrap();
assert!(
holder.tasks[stage as usize].is_null(),
"Task slot must be null after cancellation"
);
assert!(
holder.tasks.iter().all(Key::is_null),
"All task slots must be null when idle"
);
}

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));
player.client.enqueue_packet(&packet).await;
player.send_client_packet(&packet).await;
}
fn resolve_node_id(node_id: NodeId, node_id_offset: usize, root_node_index: usize) -> usize {
@ -435,7 +435,7 @@ pub async fn send_bedrock_commands_packet(
};
if let crate::net::ClientPlatform::Bedrock(bedrock_client) = player.client.as_ref() {
bedrock_client.send_game_packet(&packet).await;
bedrock_client.send_packet(&packet).await;
}
}

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

@ -333,24 +333,26 @@ pub trait EntityBase: Send + Sync + NBTStorage + std::any::Any {
},
Vec::new(),
);
client.send_game_packet(&packet).await;
if let Ok(data) = client.serialize_packet(&packet) {
client.send_game_packet(data).await;
}
})
}
fn send_java_spawn_packet<'a>(&'a self, client: &'a JavaClient) -> EntityBaseFuture<'a, ()> {
Box::pin(async move {
client
.enqueue_packet(&self.get_entity().create_spawn_packet())
.await;
let spawn_packet = self.get_entity().create_spawn_packet();
if let Ok(data) = client.serialize_packet(&spawn_packet) {
client.enqueue_packet(data).await;
}
if let Some(mob) = self.get_mob()
&& let Some(metadata) = mob.mob_java_spawn_metadata(client.version.load()).await
{
client
.enqueue_packet(&CSetEntityMetadata::new(
self.get_entity().entity_id.into(),
metadata,
))
.await;
let meta_packet =
CSetEntityMetadata::new(self.get_entity().entity_id.into(), metadata);
if let Ok(meta_data) = client.serialize_packet(&meta_packet) {
client.enqueue_packet(meta_data).await;
}
}
})
}
@ -1862,15 +1864,7 @@ impl Entity {
0,
);
let world = self.world.load();
for player in world.players.load().iter() {
let center = player.get_entity().chunk_pos.load();
let view_distance = crate::world::chunker::get_view_distance(player).get() as i32;
if is_within_view_distance(chunk_pos, center, view_distance)
&& let ClientPlatform::Bedrock(client) = player.client.as_ref()
{
client.try_enqueue_packet(&packet);
}
}
world.broadcast_to_chunk_bedrock(chunk_pos, &packet);
}
pub fn update_last_pos(&self) -> Vector3<f64> {
@ -2684,10 +2678,11 @@ impl Entity {
));
}
#[expect(clippy::unused_async)]
pub async fn set_sneaking(&self, sneaking: bool) {
//assert!(self.sneaking.load(Relaxed) != sneaking);
self.sneaking.store(sneaking, Relaxed);
self.set_flag(Flag::Sneaking, sneaking).await;
self.set_flag(Flag::Sneaking, sneaking);
}
pub fn is_sneaking(&self) -> bool {
self.sneaking.load(Ordering::Relaxed)
@ -2707,31 +2702,34 @@ impl Entity {
return;
}
self.swimming.store(event.is_swimming, Relaxed);
self.set_flag(Flag::Swimming, event.is_swimming).await;
self.set_flag(Flag::Swimming, event.is_swimming);
}
}
/// Sets whether the entity is invisible and sends updated metadata.
#[expect(clippy::unused_async)]
pub async fn set_invisible(&self, invisible: bool) {
if self.invisible.load(Ordering::Relaxed) != invisible {
self.invisible.store(invisible, Relaxed);
self.set_flag(Flag::Invisible, invisible).await;
self.set_flag(Flag::Invisible, invisible);
}
}
/// Sets whether the entity is glowing and sends updated metadata.
#[expect(clippy::unused_async)]
pub async fn set_glowing(&self, glowing: bool) {
if self.glowing.load(Ordering::Relaxed) != glowing {
self.glowing.store(glowing, Ordering::Relaxed);
self.set_flag(Flag::Glowing, glowing).await;
self.set_flag(Flag::Glowing, glowing);
}
}
/// Sets whether the entity is on fire for visual and damage purposes. This is separate from `fire_ticks` which tracks the damage aspect of being on fire.
#[expect(clippy::unused_async)]
pub async fn set_on_fire(&self, on_fire: bool) {
if self.has_visual_fire.load(Ordering::Relaxed) != on_fire {
self.has_visual_fire.store(on_fire, Ordering::Relaxed);
self.set_flag(Flag::OnFire, on_fire).await;
self.set_flag(Flag::OnFire, on_fire);
}
}
@ -2840,10 +2838,11 @@ impl Entity {
]
}
#[expect(clippy::unused_async)]
pub async fn set_sprinting(&self, sprinting: bool) {
//assert!(self.sprinting.load(Relaxed) != sprinting);
self.sprinting.store(sprinting, Relaxed);
self.set_flag(Flag::Sprinting, sprinting).await;
self.set_flag(Flag::Sprinting, sprinting);
}
pub fn is_sprinting(&self) -> bool {
@ -2853,16 +2852,17 @@ impl Entity {
!self.on_ground.load(Relaxed)
}
#[expect(clippy::unused_async)]
pub async fn set_fall_flying(&self, fall_flying: bool) {
assert_ne!(self.fall_flying.load(Relaxed), fall_flying);
self.fall_flying.store(fall_flying, Relaxed);
self.set_flag(Flag::FallFlying, fall_flying).await;
self.set_flag(Flag::FallFlying, fall_flying);
}
pub fn is_fall_flying(&self) -> bool {
self.fall_flying.load(Ordering::Relaxed)
}
async fn set_flag(&self, flag: Flag, value: bool) {
fn set_flag(&self, flag: Flag, value: bool) {
let index = flag as u8;
let mask = (1i8).wrapping_shl(index as u32);
let new_je_flags = if value {
@ -2904,36 +2904,25 @@ impl Entity {
let world = self.world.load();
let chunk_pos = self.chunk_pos.load();
for player in world.players.load().iter() {
if let ClientPlatform::Bedrock(client) = player.client.as_ref() {
let center = player.get_entity().chunk_pos.load();
let view_distance =
crate::world::chunker::get_view_distance(player).get() as i32;
if is_within_view_distance(chunk_pos, center, view_distance) {
let mut metadata = EntityMetadata(std::collections::HashMap::new());
metadata.set(
entity_data_key::FLAGS,
MetadataValue::Long(self.bedrock_flags.load(Ordering::Relaxed)),
);
metadata.set(
entity_data_key::FLAGS_TWO,
MetadataValue::Long(self.bedrock_flags_two.load(Ordering::Relaxed)),
);
client
.enqueue_packet(&CSetActorData {
actor_runtime_id: VarULong(self.entity_id as u64),
metadata,
synced_properties: PropertySyncData {
int_properties: std::collections::HashMap::new(),
float_properties: std::collections::HashMap::new(),
},
tick: VarULong(0),
})
.await;
}
}
}
let mut metadata = EntityMetadata(std::collections::HashMap::new());
metadata.set(
entity_data_key::FLAGS,
MetadataValue::Long(self.bedrock_flags.load(Ordering::Relaxed)),
);
metadata.set(
entity_data_key::FLAGS_TWO,
MetadataValue::Long(self.bedrock_flags_two.load(Ordering::Relaxed)),
);
let packet = CSetActorData {
actor_runtime_id: VarULong(self.entity_id as u64),
metadata,
synced_properties: PropertySyncData {
int_properties: std::collections::HashMap::new(),
float_properties: std::collections::HashMap::new(),
},
tick: VarULong(0),
};
world.broadcast_to_chunk_bedrock(chunk_pos, &packet);
}
}
@ -2951,49 +2940,58 @@ impl Entity {
) {
let world = self.world.load();
let chunk_pos = self.chunk_pos.load();
let players = world.players.load();
for player in world.players.load().iter() {
match player.client.as_ref() {
ClientPlatform::Java(client) => {
let version = client.version.load();
if version < CURRENT_MC_VERSION {
continue;
}
// Apply Chebyshev distance check
let center = player.get_entity().chunk_pos.load();
let view_distance =
crate::world::chunker::get_view_distance(player).get() as i32;
let mut java_recipients = Vec::new();
let mut bedrock_recipients = Vec::new();
if is_within_view_distance(chunk_pos, center, view_distance) {
let mut buf = Vec::new();
for m in meta {
let _ = m.write(&mut buf, &version);
}
buf.put_u8(255);
player.client.try_enqueue_packet(&CSetEntityMetadata::new(
self.entity_id.into(),
buf.into(),
));
}
for player in players.iter() {
let center = player.get_entity().chunk_pos.load();
let view_distance = crate::world::chunker::get_view_distance(player).get() as i32;
if is_within_view_distance(chunk_pos, center, view_distance) {
match player.client.as_ref() {
ClientPlatform::Java(_) => java_recipients.push(player),
ClientPlatform::Bedrock(client) => bedrock_recipients.push(client),
}
ClientPlatform::Bedrock(client) => {
if let Some(bedrock_meta) = bedrock_meta {
let center = player.get_entity().chunk_pos.load();
let view_distance =
crate::world::chunker::get_view_distance(player).get() as i32;
}
}
if is_within_view_distance(chunk_pos, center, view_distance) {
client.try_enqueue_packet(&CSetActorData {
actor_runtime_id: VarULong(self.entity_id as u64),
metadata: EntityMetadata(bedrock_meta.0.clone()),
synced_properties: PropertySyncData {
int_properties: std::collections::HashMap::new(),
float_properties: std::collections::HashMap::new(),
},
tick: VarULong(0),
});
}
}
let recipients_by_version =
World::collect_java_recipients_by_version(java_recipients.into_iter());
for (version, recipients) in recipients_by_version {
if version < CURRENT_MC_VERSION {
continue;
}
let mut buf = Vec::new();
for m in meta {
let _ = m.write(&mut buf, &version);
}
buf.put_u8(255);
let packet = CSetEntityMetadata::new(self.entity_id.into(), buf.into());
if let Ok(packet_data) = JavaClient::serialize_packet_for_version(&packet, version) {
for recipient in recipients {
recipient.try_enqueue_packet(packet_data.clone());
}
}
}
if let Some(bedrock_meta) = bedrock_meta {
let packet = CSetActorData {
actor_runtime_id: VarULong(self.entity_id as u64),
metadata: EntityMetadata(bedrock_meta.0.clone()),
synced_properties: PropertySyncData {
int_properties: std::collections::HashMap::new(),
float_properties: std::collections::HashMap::new(),
},
tick: VarULong(0),
};
if let Ok(packet_data) =
pumpkin_protocol::bedrock::packet_encoder::serialize_packet(&packet)
{
for recipient in bedrock_recipients {
recipient.try_enqueue_packet(packet_data.clone());
}
}
}
@ -3447,7 +3445,7 @@ impl Entity {
let world = self.world.load();
let passengers_packet = CSetPassengers::new(VarInt(self.entity_id), &passenger_ids);
if let Some(player) = passenger.get_player() {
player.client.enqueue_packet(&passengers_packet).await;
player.send_client_packet(&passengers_packet).await;
world.broadcast_to_chunk_except(
chunk_pos,
&[player.get_entity().entity_uuid],
@ -3628,18 +3626,21 @@ impl Entity {
}
};
// Clean up any remaining reference to the dismounted passenger.
passenger_entity.set_pos(dismount_pos);
// Phase 2: Teleport to safety (unblocks movement)
if let Some(player) = passenger.get_player() {
if let Some(id) = teleport_id {
player.get_entity().set_pos(dismount_pos);
// Update awaiting_teleport with the real dismount position
*player.awaiting_teleport.lock().await = Some((id.into(), dismount_pos));
// Use enqueue_packet (not send_packet_now) so the teleport goes through
// Use send_client_packet so the teleport goes through
// the same packet queue as CSetPassengers, preserving send order.
// Vanilla uses DELTA | ROT flags: position absolute, delta/rotation relative.
// With rotation relative and yaw/pitch=0, the client preserves its current look.
player
.client
.enqueue_packet(&CPlayerPosition::new(
.send_client_packet(&CPlayerPosition::new(
id.into(),
dismount_pos,
Vector3::new(0.0, 0.0, 0.0),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -58,7 +58,7 @@ pub mod login;
use self::nethernet::NetherNetSession;
use crate::{
entity::player::Player,
net::{DisconnectReason, PacketHandlerResult},
net::{DisconnectReason, PacketHandlerResult, PacketRateLimiter},
plugin::api::events::world::chunk_send::ChunkSend,
server::Server,
};
@ -123,6 +123,8 @@ pub struct BedrockClient {
last_seen: Arc<AtomicCell<std::time::Instant>>,
incoming_game_packet_send: Sender<RawPacket>,
incoming_game_packet_recv: Mutex<Option<Receiver<RawPacket>>>,
/// Packet rate limiter for incoming client packets.
pub packet_limiter: PacketRateLimiter,
}
impl BedrockClient {
@ -131,6 +133,7 @@ impl BedrockClient {
session: Arc<NetherNetSession>,
address: SocketAddr,
be_clients: Arc<Mutex<HashMap<SocketAddr, Arc<Self>>>>,
packet_limiter: PacketRateLimiter,
) -> Self {
let (send, recv) = tokio::sync::mpsc::channel(4096);
let (priority_send, priority_recv) = tokio::sync::mpsc::channel(4096);
@ -159,6 +162,7 @@ impl BedrockClient {
last_seen: Arc::new(AtomicCell::new(std::time::Instant::now())),
incoming_game_packet_send: incoming_send,
incoming_game_packet_recv: Mutex::new(Some(incoming_recv)),
packet_limiter,
}
}
@ -266,7 +270,7 @@ impl BedrockClient {
}
pub async fn kick(&self, reason: DisconnectReason, message: String) {
self.send_game_packet(&CDisconnectPlayer::new(reason as i32, message))
self.send_packet(&CDisconnectPlayer::new(reason as i32, message))
.await;
self.close().await;
}
@ -280,7 +284,7 @@ impl BedrockClient {
send_packet: bool,
) {
if send_packet {
self.send_game_packet(&CDisconnectPlayer {
self.send_packet(&CDisconnectPlayer {
reason: pumpkin_protocol::codec::var_int::VarInt(reason as i32),
skip_message,
message,
@ -383,62 +387,12 @@ impl BedrockClient {
self.player.store(Arc::new(Some(player)));
}
pub async fn enqueue_packet<P: BClientPacket>(&self, packet: &P) {
let mut packet_buf = Vec::new();
match self.write_game_packet(packet, &mut packet_buf).await {
Ok(()) => {
let payload = Bytes::from(packet_buf);
let player = self.player.load_full();
if let Some(player) = player.as_ref() {
let event = player
.fire_packet_sent_event_no_obj(P::PACKET_ID, payload)
.await;
if !event.cancelled {
self.enqueue_packet_data(event.payload).await;
}
} else {
self.enqueue_packet_data(payload).await;
}
}
Err(err) => error!("Failed to write game packet: {err}"),
}
pub async fn enqueue_packet(&self, packet_data: Bytes) {
self.enqueue_packet_data(packet_data).await;
}
pub async fn enqueue_packet_internal<P: BClientPacket>(&self, packet: &P) {
let mut packet_buf = Vec::new();
match self.write_game_packet(packet, &mut packet_buf).await {
Ok(()) => self.enqueue_packet_data(packet_buf.into()).await,
Err(err) => error!("Failed to write game packet: {err}"),
}
}
pub fn try_enqueue_packet<P: BClientPacket>(&self, packet: &P) {
let mut packet_buf = Vec::new();
let mut packet_payload = Vec::new();
if let Err(err) = packet.write_packet(&mut packet_payload) {
error!("Failed to write packet for try_enqueue_packet: {err}");
return;
}
{
let Ok(network_writer) = self.network_writer.try_read() else {
debug!("Failed to lock network writer for try_enqueue_packet");
return;
};
if let Err(err) = network_writer.write_game_packet(
P::PACKET_ID as u16,
SubClient::Main,
SubClient::Main,
&packet_payload,
&mut packet_buf,
) {
error!("Failed to write game packet for try_enqueue_packet: {err}");
return;
}
}
self.try_enqueue_packet_data(packet_buf.into());
pub fn try_enqueue_packet(&self, packet_data: Bytes) {
self.try_enqueue_packet_data(packet_data);
}
/// Queues a clientbound packet to be sent to the connected client. Queued chunks are sent
@ -446,7 +400,7 @@ impl BedrockClient {
///
/// # Arguments
///
/// * `packet`: A reference to a packet object implementing the `ClientPacket` trait.
/// * `packet_data`: A `Bytes` payload representing the encoded packet.
pub async fn enqueue_packet_data(&self, packet_data: Bytes) {
if let Err(err) = self
.outgoing_packet_queue_send
@ -508,37 +462,38 @@ impl BedrockClient {
)
}
pub async fn send_game_packet<P: BClientPacket>(&self, packet: &P) {
let mut packet_buf = Vec::new();
match self.write_game_packet(packet, &mut packet_buf).await {
Ok(()) => {
let payload = Bytes::from(packet_buf);
let player = self.player.load_full();
let payload = if let Some(player) = player.as_ref() {
let event = player
.fire_packet_sent_event_no_obj(P::PACKET_ID, payload)
.await;
if event.cancelled {
return;
}
event.payload
} else {
payload
};
let (tx, rx) = oneshot::channel();
if let Err(err) = self
.outgoing_packet_priority_send
.send(OutgoingPacket::priority(payload, tx))
.await
{
if !self.is_closed() {
error!("Failed to add priority packet to the outgoing packet queue: {err}");
}
} else {
let _ = rx.await;
}
pub fn serialize_packet<P: BClientPacket>(&self, packet: &P) -> Result<Bytes, Error> {
let encoder = self.network_writer.try_read();
encoder.map_or_else(
|_| pumpkin_protocol::bedrock::packet_encoder::serialize_packet(packet),
|encoder| encoder.serialize_packet(packet),
)
}
pub async fn send_packet<P: BClientPacket>(&self, packet: &P) {
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();
if let Err(err) = self
.outgoing_packet_priority_send
.send(OutgoingPacket::priority(packet_data, tx))
.await
{
if !self.is_closed() {
error!("Failed to add priority packet to the outgoing packet queue: {err}");
}
Err(err) => error!("Failed to write game packet: {err}"),
} else {
let _ = rx.await;
}
}
@ -585,6 +540,27 @@ impl BedrockClient {
.await
.get_game_packet(&mut cursor)
.map_err(|e| Error::other(e.to_string()))?;
if !self.packet_limiter.check_packet() {
warn!(
"Bedrock client {} exceeded packet rate limit (rate: {}/s)",
self.address,
self.packet_limiter.max_rate()
);
self.kick(
DisconnectReason::Kicked,
server
.advanced_config
.networking
.bedrock
.packet_limiter
.kick_message
.clone(),
)
.await;
return Err(Error::other("Packet rate limit exceeded"));
}
self.handle_game_packet(server, game_packet).await?;
}
@ -811,7 +787,7 @@ impl BedrockClient {
}
}
if !missing_blobs.is_empty() {
self.send_game_packet(&CClientCacheMissResponse {
self.send_packet(&CClientCacheMissResponse {
blobs: &missing_blobs,
})
.await;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -167,7 +167,7 @@ impl BedrockClient {
}
} else {
let runtime_id = pumpkin_data::BlockState::to_be_network_id(state.id);
self.enqueue_packet(&CUpdateBlock::new(location, runtime_id as u32))
self.enqueue_client_packet(&CUpdateBlock::new(location, runtime_id as u32))
.await;
if matches!(action, PlayerAction::StopBreak) {
player.stop_mining().await;

View File

@ -25,7 +25,7 @@ impl BedrockClient {
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),
})
.await;

View File

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

View File

@ -2,17 +2,24 @@
use super::*;
impl JavaClient {
pub async fn handle_config_keep_alive(&self, keep_alive: SKeepAlive) {
if self.wait_for_keep_alive.load(Ordering::Relaxed)
&& keep_alive.keep_alive_id == self.keep_alive_id.load()
pub fn handle_config_keep_alive(&self, keep_alive: &SKeepAlive) {
let mut pending = self
.pending_keep_alives
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if let Some(pos) = pending
.iter()
.position(|(id, _)| *id == keep_alive.keep_alive_id)
{
pending.swap_remove(pos);
self.wait_for_keep_alive.store(false, Ordering::Relaxed);
} else if keep_alive.keep_alive_id == self.keep_alive_id.load() {
self.wait_for_keep_alive.store(false, Ordering::Relaxed);
} else {
self.kick(pumpkin_macros::translate_cross!(
translation::java::DISCONNECT_TIMEOUT,
translation::bedrock::DISCONNECT_TIMEOUT
))
.await;
debug!(
"Ignored unexpected config keep alive id {}",
keep_alive.keep_alive_id
);
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -33,7 +33,10 @@ use tracing::{debug, error, warn};
use crate::{
entity::player::ChatMode,
net::{EncryptionError, GameProfile, PacketHandlerResult, PlayerConfig, can_not_join},
net::{
EncryptionError, GameProfile, PacketHandlerResult, PacketRateLimiter, PlayerConfig,
can_not_join,
},
server::Server,
};
@ -53,11 +56,17 @@ pub struct PendingConnection {
pub gameprofile: Option<GameProfile>,
pub config: Option<PlayerConfig>,
pub brand: Option<String>,
pub packet_limiter: PacketRateLimiter,
}
impl PendingConnection {
#[must_use]
pub fn new(tcp_stream: TcpStream, address: SocketAddr, id: u64) -> Self {
pub fn new(
tcp_stream: TcpStream,
address: SocketAddr,
id: u64,
packet_limiter: PacketRateLimiter,
) -> Self {
let (read, write) = tcp_stream.into_split();
Self {
id,
@ -71,6 +80,7 @@ impl PendingConnection {
gameprofile: None,
config: None,
brand: None,
packet_limiter,
}
}
@ -172,6 +182,25 @@ impl PendingConnection {
pub async fn handle_login_sequence(&mut self, server: &Arc<Server>) -> PacketHandlerResult {
while let Some(packet) = self.get_packet().await {
if !self.packet_limiter.check_packet() {
warn!(
"Pending client {} exceeded packet rate limit (rate: {}/s)",
self.id,
self.packet_limiter.max_rate()
);
self.kick(TextComponent::text(
server
.advanced_config
.networking
.java
.packet_limiter
.kick_message
.clone(),
))
.await;
return PacketHandlerResult::Stop;
}
match self.handle_packet(server, &packet).await {
Ok(result) => {
if let Some(result) = result {

View File

@ -31,6 +31,6 @@ impl JavaClient {
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 player.gamemode.load() == GameMode::Spectator {
player.camera_target_id.store(Some(entity_id.0));
player
.client
.send_packet_now(&CSetCamera::new(entity_id))
.await;
player.send_client_packet(&CSetCamera::new(entity_id)).await;
return;
}
send_cancellable! {{

View File

@ -2,23 +2,35 @@
use super::*;
impl JavaClient {
pub async fn handle_keep_alive(&self, player: &Player, keep_alive: SKeepAlive) {
if self.wait_for_keep_alive.load(Ordering::Relaxed)
&& keep_alive.keep_alive_id == self.keep_alive_id.load()
pub fn handle_keep_alive(&self, player: &Player, keep_alive: &SKeepAlive) {
let mut pending = self
.pending_keep_alives
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if let Some(pos) = pending
.iter()
.position(|(id, _)| *id == keep_alive.keep_alive_id)
{
let ping = self.last_keep_alive_time.load().elapsed();
let (_, send_time) = pending.swap_remove(pos);
let ping = send_time.elapsed().as_millis() as u32;
// Vanilla logic
player.ping.store(
(player.ping.load(Ordering::Relaxed) * 3 + ping.as_millis() as u32) / 4,
(player.ping.load(Ordering::Relaxed) * 3 + ping) / 4,
Ordering::Relaxed,
);
self.wait_for_keep_alive.store(false, Ordering::Relaxed);
} else if keep_alive.keep_alive_id == self.keep_alive_id.load() {
let ping = self.last_keep_alive_time.load().elapsed().as_millis() as u32;
player.ping.store(
(player.ping.load(Ordering::Relaxed) * 3 + ping) / 4,
Ordering::Relaxed,
);
self.wait_for_keep_alive.store(false, Ordering::Relaxed);
} else {
self.kick(pumpkin_macros::translate_cross!(
translation::java::DISCONNECT_TIMEOUT,
translation::bedrock::DISCONNECT_TIMEOUT
))
.await;
debug!(
"Ignored unexpected or duplicate keep alive id {} from player {}",
keep_alive.keep_alive_id, player.gameprofile.name
);
}
}
}

View File

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

View File

@ -3,7 +3,7 @@ use super::*;
impl JavaClient {
pub async fn handle_play_ping_request(&self, request: SPlayPingRequest) {
self.enqueue_packet(&CPingResponse::new(request.payload))
self.enqueue_client_packet(&CPingResponse::new(request.payload))
.await;
}
}

View File

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

View File

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

View File

@ -334,7 +334,7 @@ impl JavaClient {
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;
*player.awaiting_teleport.lock().await = Some((teleport_id.into(), position));
self.enqueue_packet(&CPlayerPosition::new(
self.enqueue_client_packet(&CPlayerPosition::new(
teleport_id.into(),
player.get_entity().pos.load(),
Vector3::new(0.0, 0.0, 0.0),

View File

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

View File

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

View File

@ -17,7 +17,7 @@ impl JavaClient {
// Empty NBT compound (TAG_End = 0)
let empty_nbt = [0u8];
self.send_packet_now(&CTagQueryResponse::new(packet.transaction_id, &empty_nbt))
self.send_packet(&CTagQueryResponse::new(packet.transaction_id, &empty_nbt))
.await;
}
@ -28,7 +28,7 @@ impl JavaClient {
// Empty NBT compound (TAG_End = 0)
let empty_nbt = [0u8];
self.send_packet_now(&CTagQueryResponse::new(packet.transaction_id, &empty_nbt))
self.send_packet(&CTagQueryResponse::new(packet.transaction_id, &empty_nbt))
.await;
}
}

View File

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

View File

@ -66,7 +66,7 @@ impl JavaClient {
event;
'cancelled: {
let state_id = world.get_block_state_id(&position);
self.enqueue_packet(&CBlockUpdate::new(
self.enqueue_client_packet(&CBlockUpdate::new(
position,
VarInt(i32::from(state_id.as_u16())),
))
@ -237,7 +237,7 @@ impl JavaClient {
.await
{
Ok(Some((final_block_pos, new_state))) => {
self.send_packet_now(&CBlockUpdate::new(
self.send_packet(&CBlockUpdate::new(
final_block_pos,
VarInt(i32::from(new_state.as_u16())),
))
@ -256,7 +256,7 @@ impl JavaClient {
/// Checks if the block placed was a sign, then opens a dialog.
pub async fn send_sign_packet(&self, block_position: BlockPos, is_front_text: bool) {
self.enqueue_packet(&COpenSignEditor::new(block_position, is_front_text))
self.enqueue_client_packet(&COpenSignEditor::new(block_position, is_front_text))
.await;
}
}

View File

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

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

View File

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

View File

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

View File

@ -62,6 +62,9 @@ impl Ticker {
next_tick += tick_interval;
// Explicitly yield to tokio to allow pending network packets / I/O tasks to be processed
tokio::task::yield_now().await;
tokio::select! {
() = sleep_until(next_tick) => {},
() = STOP_INTERRUPT.cancelled() => {
@ -69,9 +72,11 @@ impl Ticker {
}
}
// Death Spiral Prevention
// Death Spiral Prevention / Catch-up Clamping
// If the server fell behind the scheduled tick, clamp next_tick to now
// so we don't run a burst of back-to-back ticks with zero sleep.
let now = Instant::now();
if now.saturating_duration_since(next_tick) > Duration::from_secs(5) {
if now > next_tick {
next_tick = now;
}
}

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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