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