chore: split packets into separate folders

This commit is contained in:
Alexander Medvedev 2026-08-13 19:08:32 +02:00
parent 05c692985f
commit 0d5f868db6
91 changed files with 6911 additions and 6510 deletions

View File

@ -1,317 +0,0 @@
use crate::{
net::{
DisconnectReason, GameProfile, PacketHandlerResult, PlayerConfig, bedrock::BedrockClient,
},
server::Server,
};
use arc_swap::ArcSwap;
use pumpkin_protocol::bedrock::{
client::{
network_settings::CNetworkSettings, play_status::CPlayStatus,
resource_pack_stack::CResourcePackStackPacket, resource_packs_info::CResourcePacksInfo,
start_game::Experiments,
},
server::{login::SLogin, request_network_settings::SRequestNetworkSettings},
};
use pumpkin_protocol::bedrock::{
client::{resource_pack_stack::ResourcePackStackEntry, resource_packs_info::ResourcePackEntry},
server::{login::ClientData, resource_pack_response::SResourcePackResponse},
};
use pumpkin_util::jwt::AuthError;
use pumpkin_util::version::BedrockMinecraftVersion;
use pumpkin_world::{CURRENT_BEDROCK_MC_PROTOCOL, CURRENT_BEDROCK_MC_VERSION};
use serde::{Deserialize, de::Error};
use serde_repr::Deserialize_repr;
use std::sync::Arc;
use thiserror::Error;
use tracing::debug;
use uuid::Uuid;
#[derive(Debug, Error)]
pub enum LoginError {
#[error("Login packet data is not valid JSON")]
InvalidTokenFormat(#[from] serde_json::Error),
#[error("JWT chain validation failed: {0}")]
ChainValidationFailed(#[from] AuthError),
#[error("The validated username is invalid")]
InvalidUsername,
#[error("Could not parse UUID from validated token")]
InvalidUuid,
#[error("Cannot accept self-signed token. Authentication is enforced by server config.")]
SelfSignedNotAllowed,
#[error("Got a guest/splitscreen login request. Currently unimplemented.")]
GuestUnimplemented,
#[error("Failed to decode extra using decode_b64_url_nopad.")]
DecodeExtraError,
}
#[derive(Deserialize_repr)]
#[repr(u8)]
enum AuthenticationType {
Full,
Guest,
SelfSigned,
}
#[derive(Deserialize)]
#[serde(rename_all = "PascalCase")]
struct AuthPayload {
authentication_type: AuthenticationType,
token: String,
}
/// Verifies OIDC tokens for Bedrock 1.26.10+ clients.
fn verify_oidc_token_path(
server: &Server,
token: &str,
self_signed: bool,
) -> Result<pumpkin_util::jwt::PlayerClaims, LoginError> {
if self_signed {
pumpkin_util::jwt::verify_oidc_token_self_signed(token)
.map_err(LoginError::ChainValidationFailed)
} else {
let (issuer, jwks) =
server
.bedrock_oidc_keys
.get()
.ok_or(LoginError::ChainValidationFailed(
AuthError::PublicKeyBuild("OIDC keys not initialized".into()),
))?;
pumpkin_util::jwt::verify_oidc_token(token, issuer, jwks)
.map_err(LoginError::ChainValidationFailed)
}
}
impl BedrockClient {
pub async fn handle_request_network_settings(
&self,
packet: SRequestNetworkSettings,
server: &Server,
) {
if packet.protocol_version < CURRENT_BEDROCK_MC_PROTOCOL as i32 {
self.send_game_packet(&CPlayStatus::OutdatedClient).await;
return;
} else if packet.protocol_version > CURRENT_BEDROCK_MC_PROTOCOL as i32 {
self.send_game_packet(&CPlayStatus::OutdatedServer).await;
return;
}
self.version.store(BedrockMinecraftVersion::from_protocol(
packet.protocol_version as u32,
));
let compression = server
.advanced_config
.networking
.bedrock
.compression
.info
.clone();
self.send_game_packet(&CNetworkSettings::new(
compression.threshold as u16,
0,
false,
0,
0.0,
))
.await;
self.set_compression(compression).await;
}
pub async fn handle_login(
self: &Arc<Self>,
packet: SLogin,
server: &Server,
) -> Result<PacketHandlerResult, LoginError> {
self.try_handle_login(packet, server).await
}
pub async fn try_handle_login(
self: &Arc<Self>,
packet: SLogin,
server: &Server,
) -> Result<PacketHandlerResult, LoginError> {
let auth_payload: AuthPayload = serde_json::from_slice(&packet.jwt)?;
let player_data = if server.advanced_config.networking.bedrock.online_mode {
match auth_payload.authentication_type {
AuthenticationType::Full => {
verify_oidc_token_path(server, &auth_payload.token, false)?
}
AuthenticationType::SelfSigned => {
if server
.advanced_config
.networking
.bedrock
.authentication
.enabled
{
return Err(LoginError::SelfSignedNotAllowed);
}
verify_oidc_token_path(server, &auth_payload.token, true)?
}
AuthenticationType::Guest => {
return Err(LoginError::GuestUnimplemented);
}
}
} else {
pumpkin_util::jwt::extract_oidc_token_player_claims(&auth_payload.token)?
};
let raw_token_str = std::str::from_utf8(&packet.raw_token).map_err(|_| {
LoginError::InvalidTokenFormat(serde_json::Error::custom(
"raw_token is not valid UTF-8",
))
})?; // You'll need to add a string conversion error to LoginError, or handle it cleanly.
let mut parts = raw_token_str.split('.');
let _header = parts.next().ok_or(AuthError::InvalidTokenFormat)?;
let payload_b64 = parts.next().ok_or(AuthError::InvalidTokenFormat)?;
let payload_bytes = pumpkin_util::jwt::decode_b64_url_nopad(payload_b64)
.map_err(|_| LoginError::DecodeExtraError)?;
let client_data: ClientData = serde_json::from_slice(&payload_bytes)?;
let real_name = player_data.display_name;
// IMPORTANT: Bedrock allows spaces in names. While we could support this, it would significantly complicate parsing player arguments in commands, so we don't
let under_score_name = real_name.replace(' ', "_");
let profile = GameProfile {
id: Uuid::parse_str(&player_data.uuid).map_err(|_| LoginError::InvalidUuid)?,
name: under_score_name,
properties: ArcSwap::new(Arc::new(Vec::new())),
profile_actions: None,
};
let login_public_key = pumpkin_util::jwt::extract_cpk_from_token(&auth_payload.token)
.map_err(LoginError::ChainValidationFailed)?;
if self
.nethernet_public_key()
.is_some_and(|public_key| public_key != &login_public_key)
{
return Err(LoginError::ChainValidationFailed(
AuthError::PublicKeyBuild(
"NetherNet and Bedrock login identities do not match".into(),
),
));
}
self.enqueue_packet_internal(&CPlayStatus::LoginSuccess)
.await;
let br_config = &server.advanced_config.resource_pack.bedrock;
let mut entries = Vec::new();
if br_config.enabled {
for pack in &br_config.packs {
entries.push(ResourcePackEntry {
uuid: pack.uuid,
version: pack.version.clone(),
size: pack.size,
download_url: pack.download_url.clone(),
content_key: pack.content_key.clone(),
sub_pack_name: pack.sub_pack_name.clone(),
content_id: pack.content_id.clone(),
has_scripts: pack.has_scripts,
addon_pack: pack.addon_pack,
rtx_enabled: pack.rtx_enabled,
});
}
}
let packs_info = CResourcePacksInfo {
resource_pack_required: br_config.force,
has_addon_packs: false,
has_scripts: false,
is_vibrant_visuals_force_disabled: false,
world_template_id: uuid::Uuid::nil(),
world_template_version: String::new(),
resource_packs: entries,
};
self.enqueue_packet_internal(&packs_info).await;
let new_config = PlayerConfig {
locale: client_data.language_code.clone(),
..Default::default()
};
self.client_data
.store(std::sync::Arc::new(Some(std::sync::Arc::new(client_data))));
Ok(PacketHandlerResult::ReadyToPlay(profile, new_config))
}
pub async fn handle_resource_pack_response(
&self,
packet: SResourcePackResponse,
server: &Arc<Server>,
) {
// TODO: warn & ignore if the player is already spawned in
match packet.response {
SResourcePackResponse::STATUS_REFUSED => {
debug!("Bedrock: SResourcePackResponse::STATUS_REFUSED");
self.kick(
DisconnectReason::ResourcePackProblem,
"You must accept resource packs to join this server.".into(),
)
.await;
}
SResourcePackResponse::STATUS_SEND_PACKS => {
debug!("Bedrock: SResourcePackResponse::STATUS_SEND_PACKS");
// TODO: send packs
}
SResourcePackResponse::STATUS_HAVE_ALL_PACKS => {
debug!("Bedrock: SResourcePackResponse::STATUS_HAVE_ALL_PACKS");
let br_config = &server.advanced_config.resource_pack.bedrock;
// Convert your config packs into protocol stack entries
let resource_packs = if br_config.enabled {
br_config
.packs
.iter()
.map(|pack| ResourcePackStackEntry {
uuid: pack.uuid.to_string(),
version: pack.version.clone(),
sub_pack_name: String::new(),
})
.collect()
} else {
Vec::new()
};
self.enqueue_packet_internal(&CResourcePackStackPacket::new(
br_config.force,
resource_packs,
CURRENT_BEDROCK_MC_VERSION.to_string(),
Experiments {
names_size: 0,
experiments_ever_toggled: false,
},
false,
))
.await;
}
SResourcePackResponse::STATUS_COMPLETED => {
debug!("Bedrock: SResourcePackResponse::STATUS_COMPLETED");
let player = self.player.load_full();
if let Some(player) = player.as_ref() {
player
.world()
.spawn_bedrock_player(&server.basic_config, player.clone(), server)
.await;
} else {
tracing::error!(
"Got SResourcePackResponse::STATUS_COMPLETED before authentication was completed."
);
self.kick(DisconnectReason::Disconnected, String::new())
.await;
}
}
_ => {
tracing::error!("Bedrock: SResourcePackResponse bad response type");
self.kick(DisconnectReason::Disconnected, String::new())
.await;
}
}
}
}

View File

@ -0,0 +1,126 @@
#[allow(clippy::wildcard_imports)]
use super::*;
impl BedrockClient {
pub async fn handle_login(
self: &Arc<Self>,
packet: SLogin,
server: &Server,
) -> Result<PacketHandlerResult, LoginError> {
self.try_handle_login(packet, server).await
}
pub async fn try_handle_login(
self: &Arc<Self>,
packet: SLogin,
server: &Server,
) -> Result<PacketHandlerResult, LoginError> {
let auth_payload: AuthPayload = serde_json::from_slice(&packet.jwt)?;
let player_data = if server.advanced_config.networking.bedrock.online_mode {
match auth_payload.authentication_type {
AuthenticationType::Full => {
verify_oidc_token_path(server, &auth_payload.token, false)?
}
AuthenticationType::SelfSigned => {
if server
.advanced_config
.networking
.bedrock
.authentication
.enabled
{
return Err(LoginError::SelfSignedNotAllowed);
}
verify_oidc_token_path(server, &auth_payload.token, true)?
}
AuthenticationType::Guest => {
return Err(LoginError::GuestUnimplemented);
}
}
} else {
pumpkin_util::jwt::extract_oidc_token_player_claims(&auth_payload.token)?
};
let raw_token_str = std::str::from_utf8(&packet.raw_token).map_err(|_| {
LoginError::InvalidTokenFormat(serde_json::Error::custom(
"raw_token is not valid UTF-8",
))
})?; // You'll need to add a string conversion error to LoginError, or handle it cleanly.
let mut parts = raw_token_str.split('.');
let _header = parts.next().ok_or(AuthError::InvalidTokenFormat)?;
let payload_b64 = parts.next().ok_or(AuthError::InvalidTokenFormat)?;
let payload_bytes = pumpkin_util::jwt::decode_b64_url_nopad(payload_b64)
.map_err(|_| LoginError::DecodeExtraError)?;
let client_data: ClientData = serde_json::from_slice(&payload_bytes)?;
let real_name = player_data.display_name;
// IMPORTANT: Bedrock allows spaces in names. While we could support this, it would significantly complicate parsing player arguments in commands, so we don't
let under_score_name = real_name.replace(' ', "_");
let profile = GameProfile {
id: Uuid::parse_str(&player_data.uuid).map_err(|_| LoginError::InvalidUuid)?,
name: under_score_name,
properties: ArcSwap::new(Arc::new(Vec::new())),
profile_actions: None,
};
let login_public_key = pumpkin_util::jwt::extract_cpk_from_token(&auth_payload.token)
.map_err(LoginError::ChainValidationFailed)?;
if self
.nethernet_public_key()
.is_some_and(|public_key| public_key != &login_public_key)
{
return Err(LoginError::ChainValidationFailed(
AuthError::PublicKeyBuild(
"NetherNet and Bedrock login identities do not match".into(),
),
));
}
self.enqueue_packet_internal(&CPlayStatus::LoginSuccess)
.await;
let br_config = &server.advanced_config.resource_pack.bedrock;
let mut entries = Vec::new();
if br_config.enabled {
for pack in &br_config.packs {
entries.push(ResourcePackEntry {
uuid: pack.uuid,
version: pack.version.clone(),
size: pack.size,
download_url: pack.download_url.clone(),
content_key: pack.content_key.clone(),
sub_pack_name: pack.sub_pack_name.clone(),
content_id: pack.content_id.clone(),
has_scripts: pack.has_scripts,
addon_pack: pack.addon_pack,
rtx_enabled: pack.rtx_enabled,
});
}
}
let packs_info = CResourcePacksInfo {
resource_pack_required: br_config.force,
has_addon_packs: false,
has_scripts: false,
is_vibrant_visuals_force_disabled: false,
world_template_id: uuid::Uuid::nil(),
world_template_version: String::new(),
resource_packs: entries,
};
self.enqueue_packet_internal(&packs_info).await;
let new_config = PlayerConfig {
locale: client_data.language_code.clone(),
..Default::default()
};
self.client_data
.store(std::sync::Arc::new(Some(std::sync::Arc::new(client_data))));
Ok(PacketHandlerResult::ReadyToPlay(profile, new_config))
}
}

View File

@ -0,0 +1,89 @@
use crate::{
net::{
DisconnectReason, GameProfile, PacketHandlerResult, PlayerConfig, bedrock::BedrockClient,
},
server::Server,
};
use arc_swap::ArcSwap;
use pumpkin_protocol::bedrock::{
client::{
network_settings::CNetworkSettings, play_status::CPlayStatus,
resource_pack_stack::CResourcePackStackPacket, resource_packs_info::CResourcePacksInfo,
start_game::Experiments,
},
server::{login::SLogin, request_network_settings::SRequestNetworkSettings},
};
use pumpkin_protocol::bedrock::{
client::{resource_pack_stack::ResourcePackStackEntry, resource_packs_info::ResourcePackEntry},
server::{login::ClientData, resource_pack_response::SResourcePackResponse},
};
use pumpkin_util::jwt::AuthError;
use pumpkin_util::version::BedrockMinecraftVersion;
use pumpkin_world::{CURRENT_BEDROCK_MC_PROTOCOL, CURRENT_BEDROCK_MC_VERSION};
use serde::{Deserialize, de::Error};
use serde_repr::Deserialize_repr;
use std::sync::Arc;
use thiserror::Error;
use tracing::debug;
use uuid::Uuid;
#[derive(Debug, Error)]
pub enum LoginError {
#[error("Login packet data is not valid JSON")]
InvalidTokenFormat(#[from] serde_json::Error),
#[error("JWT chain validation failed: {0}")]
ChainValidationFailed(#[from] AuthError),
#[error("The validated username is invalid")]
InvalidUsername,
#[error("Could not parse UUID from validated token")]
InvalidUuid,
#[error("Cannot accept self-signed token. Authentication is enforced by server config.")]
SelfSignedNotAllowed,
#[error("Got a guest/splitscreen login request. Currently unimplemented.")]
GuestUnimplemented,
#[error("Failed to decode extra using decode_b64_url_nopad.")]
DecodeExtraError,
}
#[derive(Deserialize_repr)]
#[repr(u8)]
enum AuthenticationType {
Full,
Guest,
SelfSigned,
}
#[derive(Deserialize)]
#[serde(rename_all = "PascalCase")]
struct AuthPayload {
authentication_type: AuthenticationType,
token: String,
}
/// Verifies OIDC tokens for Bedrock 1.26.10+ clients.
fn verify_oidc_token_path(
server: &Server,
token: &str,
self_signed: bool,
) -> Result<pumpkin_util::jwt::PlayerClaims, LoginError> {
if self_signed {
pumpkin_util::jwt::verify_oidc_token_self_signed(token)
.map_err(LoginError::ChainValidationFailed)
} else {
let (issuer, jwks) =
server
.bedrock_oidc_keys
.get()
.ok_or(LoginError::ChainValidationFailed(
AuthError::PublicKeyBuild("OIDC keys not initialized".into()),
))?;
pumpkin_util::jwt::verify_oidc_token(token, issuer, jwks)
.map_err(LoginError::ChainValidationFailed)
}
}
#[allow(clippy::module_inception)]
pub mod login;
pub mod request_network_settings;
pub mod resource_pack_response;

View File

@ -0,0 +1,39 @@
#[allow(clippy::wildcard_imports)]
use super::*;
impl BedrockClient {
pub async fn handle_request_network_settings(
&self,
packet: SRequestNetworkSettings,
server: &Server,
) {
if packet.protocol_version < CURRENT_BEDROCK_MC_PROTOCOL as i32 {
self.send_game_packet(&CPlayStatus::OutdatedClient).await;
return;
} else if packet.protocol_version > CURRENT_BEDROCK_MC_PROTOCOL as i32 {
self.send_game_packet(&CPlayStatus::OutdatedServer).await;
return;
}
self.version.store(BedrockMinecraftVersion::from_protocol(
packet.protocol_version as u32,
));
let compression = server
.advanced_config
.networking
.bedrock
.compression
.info
.clone();
self.send_game_packet(&CNetworkSettings::new(
compression.threshold as u16,
0,
false,
0,
0.0,
))
.await;
self.set_compression(compression).await;
}
}

View File

@ -0,0 +1,78 @@
#[allow(clippy::wildcard_imports)]
use super::*;
impl BedrockClient {
pub async fn handle_resource_pack_response(
&self,
packet: SResourcePackResponse,
server: &Arc<Server>,
) {
// TODO: warn & ignore if the player is already spawned in
match packet.response {
SResourcePackResponse::STATUS_REFUSED => {
debug!("Bedrock: SResourcePackResponse::STATUS_REFUSED");
self.kick(
DisconnectReason::ResourcePackProblem,
"You must accept resource packs to join this server.".into(),
)
.await;
}
SResourcePackResponse::STATUS_SEND_PACKS => {
debug!("Bedrock: SResourcePackResponse::STATUS_SEND_PACKS");
// TODO: send packs
}
SResourcePackResponse::STATUS_HAVE_ALL_PACKS => {
debug!("Bedrock: SResourcePackResponse::STATUS_HAVE_ALL_PACKS");
let br_config = &server.advanced_config.resource_pack.bedrock;
// Convert your config packs into protocol stack entries
let resource_packs = if br_config.enabled {
br_config
.packs
.iter()
.map(|pack| ResourcePackStackEntry {
uuid: pack.uuid.to_string(),
version: pack.version.clone(),
sub_pack_name: String::new(),
})
.collect()
} else {
Vec::new()
};
self.enqueue_packet_internal(&CResourcePackStackPacket::new(
br_config.force,
resource_packs,
CURRENT_BEDROCK_MC_VERSION.to_string(),
Experiments {
names_size: 0,
experiments_ever_toggled: false,
},
false,
))
.await;
}
SResourcePackResponse::STATUS_COMPLETED => {
debug!("Bedrock: SResourcePackResponse::STATUS_COMPLETED");
let player = self.player.load_full();
if let Some(player) = player.as_ref() {
player
.world()
.spawn_bedrock_player(&server.basic_config, player.clone(), server)
.await;
} else {
tracing::error!(
"Got SResourcePackResponse::STATUS_COMPLETED before authentication was completed."
);
self.kick(DisconnectReason::Disconnected, String::new())
.await;
}
}
_ => {
tracing::error!("Bedrock: SResourcePackResponse bad response type");
self.kick(DisconnectReason::Disconnected, String::new())
.await;
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,32 @@
#[allow(clippy::wildcard_imports)]
use super::*;
impl BedrockClient {
pub async fn handle_animate(&self, player: &Arc<Player>, _server: &Server, packet: &SAnimate) {
if !player.has_client_loaded() {
return;
}
let entity = &player.get_entity();
let world = entity.world.load();
let java_animation = match packet.action {
AnimateAction::NoAction => None,
AnimateAction::SwingArm => Some(Animation::SwingMainArm),
AnimateAction::WakeUp => Some(Animation::LeaveBed),
AnimateAction::CriticalHit => Some(Animation::CriticalEffect),
AnimateAction::MagicCriticalHit => Some(Animation::MagicCriticaleffect),
};
if let Some(animation) = java_animation {
let je_packet = CEntityAnimation::new(VarInt(entity.entity_id), animation);
let be_packet = SAnimate {
action: packet.action,
runtime_entity_id: VarULong(entity.entity_id as u64),
data: 0.0,
swing_source: None,
};
world.broadcast_editioned(&je_packet, &be_packet).await;
}
}
}

View File

@ -0,0 +1,111 @@
#[allow(clippy::wildcard_imports)]
use super::*;
impl BedrockClient {
#[allow(clippy::too_many_lines)]
pub async fn handle_block_pick_request(&self, player: &Arc<Player>, packet: SBlockPickRequest) {
if !player.can_interact_with_block_at(&packet.block_pos, 1.0) {
return;
}
let world = player.world();
let block = world.get_block(&packet.block_pos);
if block.item_id == 0 {
return;
}
let Some(item) = pumpkin_data::item::Item::from_id(block.item_id) else {
return;
};
let stack = ItemStack::new(1, item);
let target_hotbar_slot = packet.hotbar_slot as usize;
if target_hotbar_slot >= 9 {
return;
}
let slot_with_stack = player.inventory().get_slot_with_stack(&stack).await;
if slot_with_stack != -1 {
if pumpkin_inventory::player::player_inventory::PlayerInventory::is_valid_hotbar_index(
slot_with_stack as usize,
) {
if slot_with_stack as usize != target_hotbar_slot {
let target_stack = player.inventory().get_stack(target_hotbar_slot).await;
let source_stack = player.inventory().get_stack(slot_with_stack as usize).await;
player
.inventory()
.set_stack(target_hotbar_slot, source_stack)
.await;
player
.inventory()
.set_stack(slot_with_stack as usize, target_stack)
.await;
}
} else {
let target_stack = player.inventory().get_stack(target_hotbar_slot).await;
let source_stack = player.inventory().get_stack(slot_with_stack as usize).await;
player
.inventory()
.set_stack(target_hotbar_slot, source_stack)
.await;
player
.inventory
.set_stack(slot_with_stack as usize, target_stack)
.await;
}
} else if player.gamemode.load() == GameMode::Creative {
player.inventory.set_stack(target_hotbar_slot, stack).await;
} else {
return;
}
player.inventory.set_selected_slot(target_hotbar_slot as u8);
// Send hotbar updates
player
.client
.enqueue_packet_editioned(
&CSetSelectedSlot::new(player.inventory.get_selected_slot() as i8),
&CPlayerHotbar {
selected_slot: VarUInt(player.inventory.get_selected_slot() as u32),
container_id: 0,
should_select_block: true,
},
)
.await;
// Send screen handler / Java inventory updates
player
.player_screen_handler
.lock()
.await
.send_content_updates()
.await;
// Sync main hand equipment to other players
let stack_in_hand = player.inventory().held_item().await;
let equipment = &[(EquipmentSlot::MAIN_HAND, stack_in_hand)];
player.living_entity.send_equipment_changes(equipment);
// Sync bedrock inventory updates
self.enqueue_packet(&CInventoryContent {
container_id: VarUInt(0),
slots: player
.inventory()
.main_inventory
.read()
.await
.iter()
.map(NetworkItemStackDescriptor::from)
.collect(),
full_container_name: FullContainerName {
container_name: ContainerName::Inventory,
dynamic_id: None,
},
storage_item: NetworkItemStackDescriptor::default(),
})
.await;
}
}

View File

@ -0,0 +1,47 @@
#[allow(clippy::wildcard_imports)]
use super::*;
impl BedrockClient {
pub async fn handle_chat_command(
&self,
player: &Arc<Player>,
server: &Arc<Server>,
packet: SCommandRequest<'_>,
) {
let player_clone = player.clone();
let server_clone = server.clone();
let command = packet.command.strip_prefix('/').unwrap_or(&packet.command);
send_cancellable! {{
server;
PlayerCommandSendEvent {
player: player.clone(),
command: command.to_string(),
cancelled: false
};
'after: {
let command = event.command;
let command_clone = command.clone();
// Some commands can take a long time to execute. If they do, they block packet processing for the player.
// That's why we will spawn a task instead.
server.spawn_task(async move {
let dispatcher = server_clone.command_dispatcher.read().await;
dispatcher.handle_command(
&player_clone.get_command_source(&server_clone).await,
&command_clone
).await;
});
if server.advanced_config.commands.log_console {
info!(
"Player ({}): executed command /{}",
player.gameprofile.name,
command
);
}
}
}}
}
}

View File

@ -0,0 +1,55 @@
#[allow(clippy::wildcard_imports)]
use super::*;
impl BedrockClient {
pub async fn handle_chat_message(
&self,
server: &Arc<Server>,
player: &Arc<Player>,
packet: SText<'_>,
) {
let gameprofile = &player.gameprofile;
send_cancellable! {{
server;
PlayerChatEvent::new(player.clone(), packet.message.into_owned(), vec![]);
'after: {
info!("<chat> {}: {}", gameprofile.name, event.message);
let config = &server.advanced_config;
let message = match seasonal_events::modify_chat_message(&event.message, config) {
Some(m) => m,
None => event.message.clone(),
};
let decorated_message = TextComponent::chat_decorated(
&config.chat.format,
&gameprofile.name,
&message,
);
let entity = &player.get_entity();
if server.basic_config.allow_chat_reports {
//TODO Alex help, what is this?
//world.broadcast_secure_player_chat(player, &message, decorated_message).await;
} else {
let je_packet = CSystemChatMessage::new(
&decorated_message,
false,
);
let be_packet = SText::chat_with_xuid(
message,
gameprofile.name.clone(),
packet.xuid.into_owned(),
packet.filtered_message.map(std::borrow::Cow::into_owned),
);
entity.world.load().broadcast_editioned(&je_packet, &be_packet).await;
}
}
}}
}
}

View File

@ -0,0 +1,49 @@
#[allow(clippy::wildcard_imports)]
use super::*;
impl BedrockClient {
pub async fn handle_container_close(&self, player: &Arc<Player>, packet: SContainerClose) {
if packet.container_id == 0 || packet.container_id == 0xff {
self.inventory_opened.store(false, Ordering::Relaxed);
}
player.on_handled_screen_closed().await;
self.enqueue_packet(&SContainerClose {
container_id: packet.container_id,
container_type: packet.container_type,
server_initiated: false,
})
.await;
// Sync the cursor (make it empty) to Bedrock client
self.enqueue_packet(&CInventoryContent {
container_id: VarUInt(59), // Cursor container ID
slots: vec![NetworkItemStackDescriptor::default()],
full_container_name: FullContainerName {
container_name: ContainerName::Cursor,
dynamic_id: None,
},
storage_item: NetworkItemStackDescriptor::default(),
})
.await;
// Sync the inventory content to Bedrock client
self.enqueue_packet(&CInventoryContent {
container_id: VarUInt(0), // player inventory
slots: player
.inventory()
.main_inventory
.read()
.await
.iter()
.map(NetworkItemStackDescriptor::from)
.collect(),
full_container_name: FullContainerName {
container_name: ContainerName::Inventory,
dynamic_id: None,
},
storage_item: NetworkItemStackDescriptor::default(),
})
.await;
}
}

View File

@ -0,0 +1,34 @@
#[allow(clippy::wildcard_imports)]
use super::*;
impl BedrockClient {
pub async fn handle_emote(&self, player: &Arc<Player>, _server: &Server, packet: SEmote<'_>) {
if !player.has_client_loaded() {
return;
}
tracing::info!(
"handle_emote: player={} packet={:?}",
player.gameprofile.name,
packet
);
let entity = &player.living_entity.entity;
let world = entity.world.load();
let mut broadcast_packet = packet;
broadcast_packet.runtime_entity_id = VarULong(entity.entity_id as u64);
broadcast_packet.flags |= pumpkin_protocol::bedrock::server::emote::EMOTE_FLAG_SERVER_SIDE;
world
.broadcast_packet_except_editioned(
&[player.gameprofile.id],
&CEntityAnimation::new(
VarInt(entity.entity_id),
Animation::SwingMainArm, // Fallback for Java? Or just ignore
),
&broadcast_packet,
)
.await;
}
}

View File

@ -0,0 +1,12 @@
#[allow(clippy::wildcard_imports)]
use super::*;
impl BedrockClient {
pub fn handle_emote_list(&self, player: &Arc<Player>, _server: &Server, packet: &SEmoteList) {
tracing::info!(
"handle_emote_list: player={} packet={:?}",
player.gameprofile.name,
packet
);
}
}

View File

@ -0,0 +1,31 @@
#[allow(clippy::wildcard_imports)]
use super::*;
impl BedrockClient {
pub async fn handle_interaction(&self, player: &Arc<Player>, packet: SInteraction) {
match packet.action {
Action::OpenInventory => {
if self.inventory_opened.load(Ordering::Relaxed) {
return;
}
self.inventory_opened.store(true, Ordering::Relaxed);
self.enqueue_packet(&CContainerOpen {
container_id: 0,
container_type: 0xff,
position: BlockPos::ZERO,
target_entity_id: VarLong(-1),
})
.await;
}
// No longer used in newer versions
Action::Attack => {
let target_runtime_id = packet.target_runtime_id.0 as i32;
let world = player.world();
if let Some(target) = world.get_entity_by_id(target_runtime_id) {
player.attack(target).await;
}
}
_ => {}
}
}
}

View File

@ -0,0 +1,457 @@
#[allow(clippy::wildcard_imports)]
use super::*;
impl BedrockClient {
#[allow(clippy::too_many_lines, clippy::collapsible_if, clippy::unreachable)]
pub async fn handle_inventory_action(
&self,
player: &Arc<Player>,
packet: SInventoryTransaction,
) {
tracing::debug!("handle_inventory_action: packet={:?}", packet);
let mut inventory_updated = false;
let mut updates = Vec::new();
let result = 0u8;
if packet.actions.is_empty() && packet.legacy_request_id.0 != 0 {
let mut player_screen_handler = player.player_screen_handler.lock().await;
for legacy_slot in &packet.legacy_set_item_slots {
let mapped_window_id = match legacy_slot.container_id {
28 | 29 => 0, // HotBar or Inventory
6 | 120 => 120, // Armor
34 | 119 => 119, // Offhand
other => other as i32,
};
for &slot_id in &legacy_slot.slots {
if let Some(screen_slot) =
map_bedrock_slot_to_screen_handler(mapped_window_id, slot_id as u32)
{
let current_stack = player_screen_handler
.get_slot(screen_slot)
.get_cloned_stack()
.await;
if !current_stack.is_empty() {
player.drop_item(current_stack.clone()).await;
player_screen_handler
.get_slot(screen_slot)
.set_stack(ItemStack::EMPTY.clone())
.await;
player_screen_handler
.set_received_stack(screen_slot, ItemStack::EMPTY.clone());
record_update(
&mut updates,
FullContainerName {
container_name: match legacy_slot.container_id {
28 => ContainerName::HotBar,
_ => ContainerName::Inventory,
},
dynamic_id: None,
},
slot_id,
ItemStack::EMPTY,
);
inventory_updated = true;
}
}
}
}
player_screen_handler.send_content_updates().await;
}
for action in &packet.actions {
use pumpkin_protocol::bedrock::server::inventory_transaction::InventoryActionSource;
let source_type = InventoryActionSource::from(action.source_type);
if source_type == InventoryActionSource::World {
let old_stack = descriptor_to_stack(&action.old_item);
let new_stack = descriptor_to_stack(&action.new_item);
if old_stack.is_empty() && !new_stack.is_empty() {
player.drop_item(new_stack).await;
}
} else if let Some(window_id) = action.window_id {
if let Some(screen_slot) =
map_bedrock_slot_to_screen_handler(window_id, action.inventory_slot)
{
let item_stack = descriptor_to_stack(&action.new_item);
let mut player_screen_handler = player.player_screen_handler.lock().await;
let is_armor_equipped = player_screen_handler
.get_slot(screen_slot)
.get_stack()
.await
.are_equal(&item_stack);
if !is_armor_equipped {
if (5..9).contains(&screen_slot) {
player
.enqueue_equipment_change(
&match screen_slot {
5 => EquipmentSlot::HEAD,
6 => EquipmentSlot::CHEST,
7 => EquipmentSlot::LEGS,
8 => EquipmentSlot::FEET,
_ => unreachable!(),
},
&item_stack,
)
.await;
} else if (36..45).contains(&screen_slot) {
let hotbar_slot = screen_slot - 36;
if player.inventory().get_selected_slot() == hotbar_slot as u8 {
let equipment = &[(EquipmentSlot::MAIN_HAND, item_stack.clone())];
player.living_entity.send_equipment_changes(equipment);
}
}
}
player_screen_handler
.get_slot(screen_slot)
.set_stack(item_stack.clone())
.await;
player_screen_handler.set_received_stack(screen_slot, item_stack);
player_screen_handler.send_content_updates().await;
inventory_updated = true;
}
}
}
if inventory_updated {
self.enqueue_packet(&CInventoryContent {
container_id: VarUInt(0),
slots: player
.inventory()
.main_inventory
.read()
.await
.iter()
.map(NetworkItemStackDescriptor::from)
.collect(),
full_container_name: FullContainerName {
container_name: ContainerName::Inventory,
dynamic_id: None,
},
storage_item: NetworkItemStackDescriptor::default(),
})
.await;
}
match packet.transaction_data {
TransactionData::Normal(_data) => {
// Actions are already applied to the inventory screen handler above.
}
TransactionData::Mismatch(_data) => {
// Actions are already applied to the inventory screen handler above.
}
TransactionData::UseItem(data) => {
let face = match data.block_face {
0 => BlockDirection::Down,
2 => BlockDirection::North,
3 => BlockDirection::South,
4 => BlockDirection::West,
5 => BlockDirection::East,
_ => BlockDirection::Up,
};
let world = player.world();
let block = world.get_block(&data.block_position);
let Some(server) = world.server.upgrade() else {
return;
};
if player.gamemode.load() == GameMode::Spectator {
// TODO: openMenu ?
return;
}
if data.action_type.0 == 0 {
// Click block
let client_stack = descriptor_to_stack(&data.item_in_hand);
let mut held_item = player.inventory().held_item().await;
if !client_stack.is_empty() {
if held_item.is_empty() || held_item.item.id != client_stack.item.id {
held_item = client_stack.clone();
}
}
let result = server
.block_registry
.use_with_item(
block,
player,
&data.block_position,
&BlockHitResult {
face: &face,
cursor_pos: &data.click_position,
},
&mut held_item,
&EquipmentSlot::MAIN_HAND,
&server,
&world,
)
.await;
if result.consumes_action() {
return;
}
if matches!(result, BlockActionResult::PassToDefaultBlockAction) {
server
.block_registry
.on_use(
block,
player,
&data.block_position,
&BlockHitResult {
face: &face,
cursor_pos: &data.click_position,
},
&server,
&world,
)
.await;
}
let mut stack = held_item;
if !stack.is_empty() {
server
.item_registry
.use_on_block(
&mut stack,
player,
data.block_position,
face,
data.click_position,
block,
&server,
)
.await;
let item_id = stack.item.id;
if let Some(placed_block) = pumpkin_data::Block::from_item_id(item_id) {
let dummy_use_item_on =
pumpkin_protocol::java::server::play::SUseItemOn {
hand: VarInt(0),
position: data.block_position,
face: VarInt(data.block_face),
cursor_pos: data.click_position,
inside_block: false,
is_against_world_border: false,
sequence: VarInt(0),
};
if let Ok(Some(_)) = server
.block_registry
.place_block(
player,
placed_block,
&server,
&dummy_use_item_on,
data.block_position,
face,
)
.await
{
if player.gamemode.load() != GameMode::Creative {
stack.decrement(1);
}
}
}
player.inventory().set_held_item(stack).await;
}
} else if data.action_type.0 == 1 {
// Click air / Use item
let client_stack = descriptor_to_stack(&data.item_in_hand);
let mut held = player.inventory.held_item().await;
if !client_stack.is_empty()
&& (held.is_empty() || held.item.id != client_stack.item.id)
{
held = client_stack.clone();
player.inventory.set_held_item(held.clone()).await;
}
let event = PlayerInteractEvent::new(
player,
InteractAction::RightClickAir,
&pumpkin_data::Block::AIR,
None,
);
let stack_for_use = held.clone();
{
let mut cooldown_active = false;
if let Some(cooldown) = held.get_use_cooldown() {
let group = cooldown
.cooldown_group
.clone()
.unwrap_or_else(|| held.item.registry_key.to_string());
if player.is_on_cooldown(&group).await {
cooldown_active = true;
}
}
if !cooldown_active {
if held.get_data_component::<ConsumableImpl>().is_some()
|| held.get_data_component::<BlocksAttacksImpl>().is_some()
{
if let Some(food) = held.get_data_component::<FoodImpl>() {
if player.abilities.lock().await.invulnerable
|| food.can_always_eat
|| player.hunger_manager.level.load() < 20
{
player
.living_entity
.set_active_hand(
Hand::Left,
held.clone(),
held.get_max_use_time(),
)
.await;
}
} else {
player
.living_entity
.set_active_hand(
Hand::Left,
held.clone(),
held.get_max_use_time(),
)
.await;
}
}
if let Some(equippable) = held.get_data_component::<EquippableImpl>() {
let inventory = player.inventory();
let mut equipment_guard = inventory.entity_equipment.lock().await;
let current_equipped = equipment_guard.get(equippable.slot);
if !current_equipped.are_items_and_components_equal(&held) {
player
.enqueue_equipment_change(equippable.slot, &held)
.await;
let equip_item = equipment_guard
.equipment
.entry(equippable.slot.clone())
.or_insert_with(|| ItemStack::EMPTY.clone());
if equip_item.is_empty() {
*equip_item = held.clone();
held.decrement_unless_creative(player.gamemode.load(), 1);
} else {
let old_held = held.clone();
held = equip_item.clone();
*equip_item = old_held;
}
player.inventory().set_held_item(held.clone()).await;
}
}
}
}
send_cancellable! {{
&server;
event;
'after: {
server.item_registry.on_use(&stack_for_use, player).await;
}
}}
}
}
TransactionData::UseItemOnEntity(data) => {
let target_runtime_id = data.target_entity_runtime_id.0 as i32;
// TODO: replace with consts, i'm too lazy
match data.action_type.0 {
// Interact / Item Interact
0 | 2 => {
let world = player.world();
if let Some(target) = world.get_entity_by_id(target_runtime_id) {
let mut stack = player.inventory().held_item().await;
if !target.interact(player, &mut stack).await {
let Some(server) = world.server.upgrade() else {
return;
};
server
.item_registry
.use_on_entity(&mut stack, player, target)
.await;
player.inventory().set_held_item(stack).await;
}
}
}
// Attack
1 => {
let world = player.world();
if let Some(target) = world.get_entity_by_id(target_runtime_id) {
player.attack(target).await;
}
}
_ => {
tracing::warn!(
"invalid UseItemOnEntity action type {}",
data.action_type.0
);
// Kick?
}
}
}
TransactionData::ReleaseItem(_data) => {
let item_in_use = player.living_entity.item_in_use.lock().await.clone();
if let Some(stack) = item_in_use {
let Some(server) = player.world().server.upgrade() else {
return;
};
server.item_registry.on_stopped_using(&stack, player).await;
}
player.living_entity.clear_active_hand().await;
}
}
if packet.legacy_request_id.0 != 0 {
use pumpkin_protocol::bedrock::client::item_stack_response::{
CItemStackResponse, ItemStackResponse, ItemStackResponseContainerInfo,
ItemStackResponseSlotInfo,
};
let mut container_infos = Vec::new();
if result == 0 {
for update in updates {
let container_info = container_infos.iter_mut().find(
|info: &&mut ItemStackResponseContainerInfo| {
info.container_name == update.container_name
},
);
let slot_info = ItemStackResponseSlotInfo {
slot: update.slot_id,
hotbar_slot: update.slot_id,
count: update.count,
item_stack_id: update.stack_id,
custom_name: String::new(),
filtered_custom_name: String::new(),
durability_correction: VarInt(0),
};
if let Some(info) = container_info {
info.slots.push(slot_info);
} else {
container_infos.push(ItemStackResponseContainerInfo {
container_name: update.container_name.clone(),
slots: vec![slot_info],
});
}
}
}
self.enqueue_packet(&CItemStackResponse {
responses: vec![ItemStackResponse {
result,
request_id: packet.legacy_request_id,
container_infos,
}],
})
.await;
}
}
}

View File

@ -0,0 +1,856 @@
#[allow(clippy::wildcard_imports)]
use super::*;
impl BedrockClient {
#[allow(clippy::too_many_lines)]
pub async fn handle_item_stack_request(
&self,
player: &Arc<Player>,
packet: pumpkin_protocol::bedrock::server::item_stack_request::SItemStackRequest,
) {
use pumpkin_protocol::bedrock::client::item_stack_response::{
CItemStackResponse, ItemStackResponse, ItemStackResponseContainerInfo,
ItemStackResponseSlotInfo,
};
use pumpkin_protocol::bedrock::server::item_stack_request::ItemStackRequestAction;
let current_screen_handler = player.current_screen_handler.lock().await.clone();
let mut screen_handler = current_screen_handler.lock().await;
let mut responses = Vec::with_capacity(packet.requests.len());
for request in packet.requests {
let mut created_item: Option<ItemStack> = None;
let mut crafting_inputs_consumed = false;
let mut updates = Vec::new();
let mut result = 0u8; // 0 = Success, 1 = Error
for action in request.actions {
match action {
ItemStackRequestAction::CraftCreative {
creative_item_id,
repetitions,
} => {
let index = (creative_item_id.0.saturating_sub(1)) as usize;
if index < pumpkin_data::bedrock_creative::CREATIVE_ENTRIES.len() {
let entry = pumpkin_data::bedrock_creative::CREATIVE_ENTRIES[index];
if let Some(mapping) =
pumpkin_data::item::JavaToBedrockItemMapping::from_bedrock(
entry.item_id,
entry.item_aux_value,
)
{
// Bedrock `repetitions` represents how many stacks to create; use the item's max stack size
let max_stack = ItemStack::static_new_java(1, mapping.java_item)
.get_max_stack_size();
let count = ((max_stack as u16) * (repetitions as u16))
.min(u8::MAX as u16)
as u8;
created_item = Some(ItemStack::new(count, mapping.java_item));
} else {
tracing::warn!(
"Failed to map bedrock item id {} and data {} to Java item",
entry.item_id,
entry.item_aux_value
);
result = 1;
break;
}
} else {
tracing::warn!(
"Creative item index {} out of bounds (len: {})",
index,
pumpkin_data::bedrock_creative::CREATIVE_ENTRIES.len()
);
result = 1;
break;
}
}
ItemStackRequestAction::Take {
count,
source,
destination,
}
| ItemStackRequestAction::Place {
count,
source,
destination,
} => {
let mut source_stack =
get_slot_stack(&*screen_handler, &source, created_item.as_ref()).await;
if source_stack.is_empty() && created_item.is_none() {
tracing::debug!("Source stack is empty in Take/Place");
result = 1;
break;
}
let count = count.min(source_stack.item_count);
if count > 0 {
let mut dest_stack = get_slot_stack(
&*screen_handler,
&destination,
created_item.as_ref(),
)
.await;
if dest_stack.is_empty() {
dest_stack = source_stack.copy_with_count(count);
} else if dest_stack.are_items_and_components_equal(&source_stack) {
dest_stack.item_count = dest_stack.item_count.saturating_add(count);
} else {
tracing::debug!(
"Destination stack is not compatible with source stack"
);
result = 1;
break;
}
source_stack.decrement(count);
if source.container_name.container_name == ContainerName::CreatedOutput
&& let Some(ref mut stack) = created_item
{
stack.decrement(count);
if stack.is_empty() {
created_item = None;
}
}
let source_stack = if source_stack.is_empty() {
ItemStack::EMPTY.clone()
} else {
source_stack
};
update_slot_stack(
player,
&mut *screen_handler,
&source,
source_stack.clone(),
)
.await;
update_slot_stack(
player,
&mut *screen_handler,
&destination,
dest_stack.clone(),
)
.await;
record_update(
&mut updates,
source.container_name.clone(),
source.slot_id,
&source_stack,
);
record_update(
&mut updates,
destination.container_name.clone(),
destination.slot_id,
&dest_stack,
);
}
}
ItemStackRequestAction::Swap { slot1, slot2 } => {
let stack1 =
get_slot_stack(&*screen_handler, &slot1, created_item.as_ref()).await;
let stack2 =
get_slot_stack(&*screen_handler, &slot2, created_item.as_ref()).await;
update_slot_stack(player, &mut *screen_handler, &slot1, stack2.clone())
.await;
update_slot_stack(player, &mut *screen_handler, &slot2, stack1.clone())
.await;
record_update(
&mut updates,
slot1.container_name.clone(),
slot1.slot_id,
&stack2,
);
record_update(
&mut updates,
slot2.container_name.clone(),
slot2.slot_id,
&stack1,
);
}
ItemStackRequestAction::Drop {
count,
source,
randomly: _,
} => {
let mut source_stack =
get_slot_stack(&*screen_handler, &source, created_item.as_ref()).await;
if source_stack.is_empty() {
result = 1;
break;
}
let count = count.min(source_stack.item_count);
if count > 0 {
let dropped_stack = source_stack.copy_with_count(count);
player.drop_item(dropped_stack).await;
source_stack.decrement(count);
let source_stack = if source_stack.is_empty() {
ItemStack::EMPTY.clone()
} else {
source_stack
};
update_slot_stack(
player,
&mut *screen_handler,
&source,
source_stack.clone(),
)
.await;
record_update(
&mut updates,
source.container_name.clone(),
source.slot_id,
&source_stack,
);
}
}
ItemStackRequestAction::Destroy { count, source }
| ItemStackRequestAction::Consume { count, source } => {
if crafting_inputs_consumed
&& source.container_name.container_name == ContainerName::CraftingInput
{
let source_stack =
get_slot_stack(&*screen_handler, &source, created_item.as_ref())
.await;
record_update(
&mut updates,
source.container_name.clone(),
source.slot_id,
&source_stack,
);
continue;
}
let mut source_stack =
get_slot_stack(&*screen_handler, &source, created_item.as_ref()).await;
if source_stack.is_empty() {
result = 1;
break;
}
let count = count.min(source_stack.item_count);
if count > 0 {
source_stack.decrement(count);
let source_stack = if source_stack.is_empty() {
ItemStack::EMPTY.clone()
} else {
source_stack
};
update_slot_stack(
player,
&mut *screen_handler,
&source,
source_stack.clone(),
)
.await;
record_update(
&mut updates,
source.container_name.clone(),
source.slot_id,
&source_stack,
);
}
}
ItemStackRequestAction::CraftRecipe {
recipe_id: _,
repetitions,
}
| ItemStackRequestAction::CraftRecipeAuto {
recipe_id: _,
repetitions,
..
} => {
if repetitions > 0 {
screen_handler.update_to_client().await;
let is_player = screen_handler.window_type().is_none();
let grid_size = if is_player { 4 } else { 9 };
let bedrock_grid_start = if is_player { 28 } else { 32 };
for i in 0..grid_size {
let grid_slot_index = 1 + i;
let grid_slot =
screen_handler.get_behaviour().slots[grid_slot_index].clone();
let grid_stack = grid_slot.get_cloned_stack().await;
tracing::info!(
"Crafting Grid slot {i} (slot index {grid_slot_index}): Item ID: {}, Count: {}",
grid_stack.item.id,
grid_stack.item_count
);
}
let output_slot = screen_handler.get_behaviour().slots[0].clone();
let output_stack = output_slot.get_cloned_stack().await;
if output_stack.is_empty()
|| repetitions > output_slot.get_max_item_count().await
{
tracing::warn!("Client sent an invalid crafting request");
result = 1;
break;
}
let mut total_crafted = output_stack.clone();
total_crafted.item_count =
total_crafted.item_count.saturating_mul(repetitions);
created_item = Some(total_crafted);
for _ in 0..repetitions {
output_slot
.on_take_item(player.as_ref(), &output_stack)
.await;
}
crafting_inputs_consumed = true;
// Record updates for all grid slots so Bedrock client is notified of consumed ingredients!
let is_player = screen_handler.window_type().is_none();
let grid_size = if is_player { 4 } else { 9 };
for i in 0..grid_size {
let grid_slot_index = 1 + i;
let grid_slot =
screen_handler.get_behaviour().slots[grid_slot_index].clone();
let grid_stack = grid_slot.get_cloned_stack().await;
record_update(
&mut updates,
FullContainerName {
container_name: ContainerName::CraftingInput,
dynamic_id: None,
},
(bedrock_grid_start + i) as u8,
&grid_stack,
);
}
}
}
ItemStackRequestAction::CraftResultsDeprecated { .. }
| ItemStackRequestAction::MineBlock { .. }
| ItemStackRequestAction::BeaconPayment { .. }
| ItemStackRequestAction::Create { .. }
| ItemStackRequestAction::LabTableCombine
| ItemStackRequestAction::Optional { .. }
| ItemStackRequestAction::Grindstone { .. }
| ItemStackRequestAction::Loom { .. }
| ItemStackRequestAction::CraftNonImplemented => {
// Successful no-ops to prevent client-side transaction rollbacks
}
}
}
let mut container_infos = Vec::new();
if result == 0 {
for update in updates {
let container_info = container_infos.iter_mut().find(
|info: &&mut ItemStackResponseContainerInfo| {
info.container_name == update.container_name
},
);
let slot_info = ItemStackResponseSlotInfo {
slot: update.slot_id,
hotbar_slot: update.slot_id,
count: update.count,
item_stack_id: update.stack_id,
custom_name: String::new(),
filtered_custom_name: String::new(),
durability_correction: VarInt(0),
};
if let Some(info) = container_info {
info.slots.push(slot_info);
} else {
container_infos.push(ItemStackResponseContainerInfo {
container_name: update.container_name,
slots: vec![slot_info],
});
}
}
}
responses.push(ItemStackResponse {
result,
request_id: request.request_id,
container_infos,
});
}
// Send updates to Java client
screen_handler.send_content_updates().await;
// Collect inventory updates if we modified player inventory
let mut inventory_updated = false;
for response in &responses {
if response.result == 0 {
for info in &response.container_infos {
if info.container_name.container_name == ContainerName::Inventory
|| info.container_name.container_name
== ContainerName::CombinedHotBarAndInventory
|| info.container_name.container_name == ContainerName::HotBar
{
inventory_updated = true;
}
}
}
}
// Send Bedrock specific responses and updates
self.enqueue_packet(&CItemStackResponse { responses }).await;
if inventory_updated {
self.enqueue_packet(&CInventoryContent {
container_id: VarUInt(0),
slots: player
.inventory()
.main_inventory
.read()
.await
.iter()
.map(NetworkItemStackDescriptor::from)
.collect(),
full_container_name: FullContainerName {
container_name: ContainerName::Inventory,
dynamic_id: None,
},
storage_item: NetworkItemStackDescriptor::default(),
})
.await;
}
}
}
#[allow(clippy::too_many_lines)]
pub(crate) fn map_bedrock_container_slot(
screen_handler: &dyn ScreenHandler,
container_name: ContainerName,
slot_id: u8,
) -> Option<usize> {
let is_player_screen = screen_handler.window_type().is_none();
let container_slots = screen_handler
.get_behaviour()
.slots
.len()
.saturating_sub(PlayerInventory::MAIN_SIZE);
match container_name {
ContainerName::HotBar => {
if is_player_screen {
Some(36 + slot_id as usize)
} else {
Some(container_slots + 27 + slot_id as usize)
}
}
ContainerName::Inventory | ContainerName::CombinedHotBarAndInventory => {
if slot_id < 9 {
if is_player_screen {
Some(36 + slot_id as usize)
} else {
Some(container_slots + 27 + slot_id as usize)
}
} else if slot_id < 36 {
if is_player_screen {
Some(slot_id as usize)
} else {
Some(container_slots + (slot_id - 9) as usize)
}
} else {
None
}
}
ContainerName::Armor => (slot_id < 4).then(|| 5 + slot_id as usize),
ContainerName::Offhand => (slot_id == 0).then_some(45),
ContainerName::Cursor => None,
ContainerName::CraftingInput => {
if is_player_screen {
if slot_id < 4 {
Some(1 + slot_id as usize)
} else if (28..32).contains(&slot_id) {
Some(1 + (slot_id - 28) as usize)
} else {
None
}
} else if screen_handler.window_type()
== Some(pumpkin_data::screen::WindowType::Crafting)
{
if slot_id < 9 {
Some(1 + slot_id as usize)
} else if (32..41).contains(&slot_id) {
Some(1 + (slot_id - 32) as usize)
} else {
None
}
} else {
None
}
}
ContainerName::CraftingOutputPreview | ContainerName::CreatedOutput => {
if is_player_screen {
Some(0)
} else if let Some(window_type) = screen_handler.window_type() {
match window_type {
pumpkin_data::screen::WindowType::Crafting => Some(0),
pumpkin_data::screen::WindowType::Stonecutter => Some(1),
pumpkin_data::screen::WindowType::Anvil
| pumpkin_data::screen::WindowType::Furnace
| pumpkin_data::screen::WindowType::BlastFurnace
| pumpkin_data::screen::WindowType::Smoker
| pumpkin_data::screen::WindowType::Grindstone
| pumpkin_data::screen::WindowType::Merchant => Some(2),
pumpkin_data::screen::WindowType::Loom
| pumpkin_data::screen::WindowType::Smithing => Some(3),
_ => None,
}
} else {
None
}
}
ContainerName::AnvilInput => matches!(
screen_handler.window_type(),
Some(pumpkin_data::screen::WindowType::Anvil)
)
.then_some(0),
ContainerName::AnvilMaterial => matches!(
screen_handler.window_type(),
Some(pumpkin_data::screen::WindowType::Anvil)
)
.then_some(1),
ContainerName::AnvilResultPreview => matches!(
screen_handler.window_type(),
Some(pumpkin_data::screen::WindowType::Anvil)
)
.then_some(2),
ContainerName::BeaconPayment => matches!(
screen_handler.window_type(),
Some(pumpkin_data::screen::WindowType::Beacon)
)
.then_some(0),
ContainerName::BrewingStandResult => (matches!(
screen_handler.window_type(),
Some(pumpkin_data::screen::WindowType::BrewingStand)
) && slot_id < 3)
.then_some(slot_id as usize),
ContainerName::BrewingStandInput => matches!(
screen_handler.window_type(),
Some(pumpkin_data::screen::WindowType::BrewingStand)
)
.then_some(3),
ContainerName::BrewingStandFuel => matches!(
screen_handler.window_type(),
Some(pumpkin_data::screen::WindowType::BrewingStand)
)
.then_some(4),
ContainerName::FurnaceIngredient
| ContainerName::BlastFurnaceIngredient
| ContainerName::SmokerIngredient => matches!(
screen_handler.window_type(),
Some(
pumpkin_data::screen::WindowType::Furnace
| pumpkin_data::screen::WindowType::BlastFurnace
| pumpkin_data::screen::WindowType::Smoker
)
)
.then_some(0),
ContainerName::FurnaceFuel => matches!(
screen_handler.window_type(),
Some(
pumpkin_data::screen::WindowType::Furnace
| pumpkin_data::screen::WindowType::BlastFurnace
| pumpkin_data::screen::WindowType::Smoker
)
)
.then_some(1),
ContainerName::FurnaceResult => matches!(
screen_handler.window_type(),
Some(
pumpkin_data::screen::WindowType::Furnace
| pumpkin_data::screen::WindowType::BlastFurnace
| pumpkin_data::screen::WindowType::Smoker
)
)
.then_some(2),
ContainerName::EnchantingInput => matches!(
screen_handler.window_type(),
Some(pumpkin_data::screen::WindowType::Enchantment)
)
.then_some(0),
ContainerName::EnchantingMaterial => matches!(
screen_handler.window_type(),
Some(pumpkin_data::screen::WindowType::Enchantment)
)
.then_some(1),
ContainerName::GrindstoneInput => matches!(
screen_handler.window_type(),
Some(pumpkin_data::screen::WindowType::Grindstone)
)
.then_some(0),
ContainerName::GrindstoneAdditional => matches!(
screen_handler.window_type(),
Some(pumpkin_data::screen::WindowType::Grindstone)
)
.then_some(1),
ContainerName::GrindstoneResultPreview => matches!(
screen_handler.window_type(),
Some(pumpkin_data::screen::WindowType::Grindstone)
)
.then_some(2),
ContainerName::LoomInput => matches!(
screen_handler.window_type(),
Some(pumpkin_data::screen::WindowType::Loom)
)
.then_some(0),
ContainerName::LoomDye => matches!(
screen_handler.window_type(),
Some(pumpkin_data::screen::WindowType::Loom)
)
.then_some(1),
ContainerName::LoomMaterial => matches!(
screen_handler.window_type(),
Some(pumpkin_data::screen::WindowType::Loom)
)
.then_some(2),
ContainerName::LoomResultPreview => matches!(
screen_handler.window_type(),
Some(pumpkin_data::screen::WindowType::Loom)
)
.then_some(3),
ContainerName::StonecutterInput => matches!(
screen_handler.window_type(),
Some(pumpkin_data::screen::WindowType::Stonecutter)
)
.then_some(0),
ContainerName::StonecutterResultPreview => matches!(
screen_handler.window_type(),
Some(pumpkin_data::screen::WindowType::Stonecutter)
)
.then_some(1),
ContainerName::CartographyInput => matches!(
screen_handler.window_type(),
Some(pumpkin_data::screen::WindowType::CartographyTable)
)
.then_some(0),
ContainerName::CartographyAdditional => matches!(
screen_handler.window_type(),
Some(pumpkin_data::screen::WindowType::CartographyTable)
)
.then_some(1),
ContainerName::CartographyResultPreview => matches!(
screen_handler.window_type(),
Some(pumpkin_data::screen::WindowType::CartographyTable)
)
.then_some(2),
ContainerName::SmithingTableTemplate => matches!(
screen_handler.window_type(),
Some(pumpkin_data::screen::WindowType::Smithing)
)
.then_some(0),
ContainerName::SmithingTableInput => matches!(
screen_handler.window_type(),
Some(pumpkin_data::screen::WindowType::Smithing)
)
.then_some(1),
ContainerName::SmithingTableMaterial => matches!(
screen_handler.window_type(),
Some(pumpkin_data::screen::WindowType::Smithing)
)
.then_some(2),
ContainerName::SmithingTableResultPreview => matches!(
screen_handler.window_type(),
Some(pumpkin_data::screen::WindowType::Smithing)
)
.then_some(3),
ContainerName::TradeIngredient1 | ContainerName::Trade2Ingredient1 => matches!(
screen_handler.window_type(),
Some(pumpkin_data::screen::WindowType::Merchant)
)
.then_some(0),
ContainerName::TradeIngredient2 | ContainerName::Trade2Ingredient2 => matches!(
screen_handler.window_type(),
Some(pumpkin_data::screen::WindowType::Merchant)
)
.then_some(1),
ContainerName::TradeResultPreview | ContainerName::Trade2ResultPreview => matches!(
screen_handler.window_type(),
Some(pumpkin_data::screen::WindowType::Merchant)
)
.then_some(2),
_ => ((slot_id as usize) < container_slots).then_some(slot_id as usize),
}
}
pub(crate) struct SlotUpdate {
pub(crate) container_name: FullContainerName,
pub(crate) slot_id: u8,
pub(crate) count: u8,
pub(crate) stack_id: VarInt,
}
pub(crate) fn record_update(
updates: &mut Vec<SlotUpdate>,
container_name: FullContainerName,
slot_id: u8,
stack: &ItemStack,
) {
let count = stack.item_count;
let stack_id = if stack.is_empty() {
VarInt(0)
} else {
VarInt(stack.uid.get())
};
if let Some(existing) = updates
.iter_mut()
.find(|u| u.container_name == container_name && u.slot_id == slot_id)
{
existing.count = count;
existing.stack_id = stack_id;
} else {
updates.push(SlotUpdate {
container_name,
slot_id,
count,
stack_id,
});
}
}
async fn get_slot_stack(
screen_handler: &dyn ScreenHandler,
slot_info: &pumpkin_protocol::bedrock::server::item_stack_request::ItemStackRequestSlotInfo,
created_item: Option<&ItemStack>,
) -> ItemStack {
if let (ContainerName::CreatedOutput, Some(stack)) =
(slot_info.container_name.container_name, created_item)
{
return stack.clone();
}
if slot_info.container_name.container_name == ContainerName::Cursor {
return screen_handler
.get_behaviour()
.cursor_stack
.lock()
.await
.clone();
}
if let Some(screen_slot) = map_bedrock_container_slot(
screen_handler,
slot_info.container_name.container_name,
slot_info.slot_id,
) {
screen_handler.get_behaviour().slots[screen_slot]
.get_cloned_stack()
.await
} else {
ItemStack::EMPTY.clone()
}
}
#[allow(clippy::unreachable)]
async fn update_slot_stack(
player: &Player,
screen_handler: &mut dyn ScreenHandler,
slot_info: &pumpkin_protocol::bedrock::server::item_stack_request::ItemStackRequestSlotInfo,
new_stack: ItemStack,
) {
if slot_info.container_name.container_name == ContainerName::Cursor {
let mut cursor_lock = screen_handler.get_behaviour().cursor_stack.lock().await;
*cursor_lock = new_stack;
return;
}
if let Some(screen_slot) = map_bedrock_container_slot(
screen_handler,
slot_info.container_name.container_name,
slot_info.slot_id,
) {
let is_player_screen = screen_handler.window_type().is_none();
if is_player_screen {
let current_stack = screen_handler.get_behaviour().slots[screen_slot]
.get_cloned_stack()
.await;
if !current_stack.are_items_and_components_equal(&new_stack) {
if (5..9).contains(&screen_slot) {
player
.enqueue_equipment_change(
&match screen_slot {
5 => EquipmentSlot::HEAD,
6 => EquipmentSlot::CHEST,
7 => EquipmentSlot::LEGS,
8 => EquipmentSlot::FEET,
_ => unreachable!(),
},
&new_stack,
)
.await;
} else if (36..45).contains(&screen_slot) {
let hotbar_slot = screen_slot - 36;
if player.inventory().get_selected_slot() == hotbar_slot as u8 {
let equipment = &[(EquipmentSlot::MAIN_HAND, new_stack.clone())];
player.living_entity.send_equipment_changes(equipment);
}
}
}
}
screen_handler.get_behaviour().slots[screen_slot]
.set_stack(new_stack.clone())
.await;
screen_handler.set_received_stack(screen_slot, new_stack);
}
}
#[cfg(test)]
mod tests {
use super::*;
use pumpkin_data::item::Item;
use pumpkin_inventory::{
build_equipment_slots, crafting::crafting_screen_handler::CraftingTableScreenHandler,
entity_equipment::EntityEquipment,
};
use tokio::sync::Mutex;
#[tokio::test]
async fn crafting_table_maps_bedrock_player_inventory_after_its_ten_slots() {
let inventory = Arc::new(PlayerInventory::new(
Arc::new(Mutex::new(EntityEquipment::new())),
Arc::new(build_equipment_slots()),
));
let handler = CraftingTableScreenHandler::new(1, &inventory, None).await;
assert_eq!(
map_bedrock_container_slot(&handler, ContainerName::Inventory, 26),
Some(27)
);
assert_eq!(
map_bedrock_container_slot(&handler, ContainerName::HotBar, 0),
Some(37)
);
assert_eq!(
map_bedrock_container_slot(&handler, ContainerName::CraftingInput, 32),
Some(1)
);
assert_eq!(
map_bedrock_container_slot(&handler, ContainerName::CraftingInput, 40),
Some(9)
);
}
#[test]
fn item_stack_response_uses_the_authoritative_stack_id() {
let stack = ItemStack::new(3, &Item::SPRUCE_DOOR);
let container = FullContainerName {
container_name: ContainerName::Cursor,
dynamic_id: None,
};
let mut updates = Vec::new();
record_update(&mut updates, container.clone(), 0, &stack);
assert_eq!(updates[0].count, 3);
assert_eq!(updates[0].stack_id, VarInt(stack.uid.get()));
record_update(&mut updates, container, 0, ItemStack::EMPTY);
assert_eq!(updates[0].count, 0);
assert_eq!(updates[0].stack_id, VarInt(0));
}
}

View File

@ -0,0 +1,42 @@
#[allow(clippy::wildcard_imports)]
use super::*;
impl BedrockClient {
pub async fn handle_mob_equipment(
&self,
_server: &Arc<Server>,
player: &Arc<Player>,
packet: SMobEquipment,
) {
player.update_last_action_time();
let slot = packet.hotbar_slot;
if slot >= 9 {
return;
}
let previous_slot = player.inventory.get_selected_slot();
let server = player.world().server.upgrade();
if let Some(server) = server {
let mut event = PlayerItemHeldEvent::new(player.clone(), previous_slot, slot);
server.plugin_manager.fire(&server, &mut event).await;
let is_cancelled = {
use crate::plugin::Cancellable;
event.cancelled()
};
if is_cancelled {
self.enqueue_packet(&CPlayerHotbar {
selected_slot: VarUInt(previous_slot as u32),
container_id: 0,
should_select_block: true,
})
.await;
return;
}
}
let inv = player.inventory();
inv.set_selected_slot(slot);
let stack = inv.held_item().await;
let equipment = &[(EquipmentSlot::MAIN_HAND, stack)];
player.living_entity.send_equipment_changes(equipment);
}
}

View File

@ -0,0 +1,154 @@
use std::{
num::{NonZero, NonZeroI32},
sync::{Arc, atomic::Ordering},
};
use pumpkin_data::{
data_component_impl::{
BlocksAttacksImpl, ConsumableImpl, EquipmentSlot, EquippableImpl, FoodImpl,
},
item_stack::ItemStack,
};
use pumpkin_inventory::{
player::player_inventory::PlayerInventory,
screen_handler::{InventoryPlayer, ScreenHandler},
};
use pumpkin_protocol::bedrock::{
client::{inventory_content::CInventoryContent, respawn::CRespawn},
network_item::{
ContainerName, FullContainerName, NetworkItemDescriptor, NetworkItemStackDescriptor,
},
respawn::RespawnState,
};
use pumpkin_protocol::{
bedrock::{
client::{
chunk_radius_update::CChunkRadiusUpdate, container_open::CContainerOpen,
player_hotbar::CPlayerHotbar, update_block::CUpdateBlock,
},
server::{
animate::{AnimateAction, SAnimate},
block_pick_request::SBlockPickRequest,
command_request::SCommandRequest,
container_close::SContainerClose,
emote::SEmote,
emote_list::SEmoteList,
interaction::{Action, SInteraction},
inventory_transaction::{SInventoryTransaction, TransactionData},
mob_equipment::SMobEquipment,
player_action::{Action as PlayerAction, SPlayerAction},
player_auth_input::{InputData, SPlayerAuthInput},
request_chunk_radius::SRequestChunkRadius,
respawn::SRespawn,
set_local_player_as_initialized::SSetLocalPlayerAsInitialized,
text::SText,
},
},
codec::{var_int::VarInt, var_long::VarLong, var_uint::VarUInt, var_ulong::VarULong},
java::client::play::{Animation, CEntityAnimation, CSetSelectedSlot, CSystemChatMessage},
};
use pumpkin_util::{GameMode, Hand, math::position::BlockPos, text::TextComponent};
use pumpkin_world::inventory::Inventory;
use pumpkin_world::world::BlockFlags;
use crate::{
block::{BlockHitResult, registry::BlockActionResult},
entity::{
EntityBase,
player::{MINE_BLOCK_EXHAUSTION, Player},
},
net::{DisconnectReason, bedrock::BedrockClient},
plugin::player::{
item_held::PlayerItemHeldEvent,
player_chat::PlayerChatEvent,
player_command_send::PlayerCommandSendEvent,
player_interact_event::{InteractAction, PlayerInteractEvent},
player_toggle_flight_event::PlayerToggleFlightEvent,
},
server::{Server, seasonal_events},
world::{BlockBreakingProgress, chunker},
};
use pumpkin_data::BlockDirection;
use tracing::{debug, info};
const MIN_PREDICTED_BREAK_PROGRESS: f32 = 0.65;
fn descriptor_to_stack(desc: &NetworkItemDescriptor) -> ItemStack {
if desc.id.0 == 0 || desc.stack_size == 0 {
ItemStack::EMPTY.clone()
} else {
pumpkin_data::item::JavaToBedrockItemMapping::from_bedrock(
desc.id.0 as i16,
desc.aux_value.0,
)
.map_or_else(
|| {
tracing::warn!(
"Failed to map bedrock item id {} and data {} to Java item",
desc.id.0,
desc.aux_value.0
);
ItemStack::EMPTY.clone()
},
|mapping| ItemStack::new(desc.stack_size as u8, mapping.java_item),
)
}
}
const fn map_bedrock_slot_to_screen_handler(window_id: i32, slot: u32) -> Option<usize> {
match window_id {
0 => {
// WINDOW_ID_INVENTORY
if slot < 9 {
// Hotbar: Bedrock 0-8 -> Screen Handler 36-44
Some(slot as usize + 36)
} else if slot < 36 {
// Main Inventory: Bedrock 9-35 -> Screen Handler 9-35
Some(slot as usize)
} else {
None
}
}
120 => {
// WINDOW_ID_ARMOUR
if slot < 4 {
// Armor: Bedrock 0-3 -> Screen Handler 5-8
Some(slot as usize + 5)
} else {
None
}
}
119 => {
// WINDOW_ID_OFF_HAND
if slot == 0 {
// Offhand: Bedrock 0 -> Screen Handler 45
Some(45)
} else {
None
}
}
_ => None,
}
}
pub mod animate;
pub mod block_pick_request;
pub mod chat_command;
pub mod chat_message;
pub mod container_close;
pub mod emote;
pub mod emote_list;
pub mod interaction;
pub mod inventory_action;
pub mod item_stack_request;
pub(super) use item_stack_request::record_update;
pub mod mob_equipment;
pub mod modal_form_response;
pub mod player_action;
pub mod player_auth_input;
pub mod player_block_action;
pub mod request_ability;
pub mod request_chunk_radius;
pub mod respawn;
pub mod set_local_player_as_initialized;

View File

@ -0,0 +1,22 @@
#[allow(clippy::wildcard_imports)]
use super::*;
impl BedrockClient {
pub async fn handle_modal_form_response(
&self,
player: &Arc<Player>,
server: &Arc<Server>,
packet: pumpkin_protocol::bedrock::server::modal_form_response::SModalFormResponse<'_>,
) {
let mut event =
crate::plugin::api::events::player::bedrock_form_response::BedrockFormResponseEvent::new(
player.clone(),
packet.form_id.0,
packet
.form_data
.filter(|data| data != "null")
.map(std::borrow::Cow::into_owned),
);
server.plugin_manager.fire(server, &mut event).await;
}
}

View File

@ -0,0 +1,213 @@
#[allow(clippy::wildcard_imports)]
use super::*;
impl BedrockClient {
#[expect(clippy::match_same_arms)]
#[expect(clippy::too_many_lines)]
pub async fn handle_player_action(
&self,
player: &Arc<Player>,
server: &Server,
packet: SPlayerAction,
) {
if !player.has_client_loaded()
|| ((player.living_entity.dead.load(Ordering::Relaxed)
|| player.living_entity.health.load() <= 0.0)
&& !matches!(packet.action, PlayerAction::Respawn))
{
return;
}
player.update_last_action_time();
match packet.action {
PlayerAction::StartBreak
| PlayerAction::CreativePlayerDestroyBlock
| PlayerAction::ContinueDestroyBlock => {
let location = packet.block_pos;
if !player.can_interact_with_block_at(&location, 1.0) {
return;
}
let entity = &player.get_entity();
let world = entity.world.load_full();
let (block, state) = world.get_block_and_state(&location);
if player.mining.load(Ordering::Relaxed)
&& *player.mining_pos.lock().await != location
{
player.stop_mining().await;
}
if player.gamemode.load() == GameMode::Creative {
let new_state = world
.break_block(
&location,
Some(player.clone()),
BlockFlags::NOTIFY_NEIGHBORS | BlockFlags::SKIP_DROPS,
)
.await;
if new_state.is_some() {
server
.block_registry
.broken(&world, block, player, &location, server, state)
.await;
}
} else if !state.is_air() {
let speed = crate::block::calc_block_breaking(player, state, block).await;
if speed >= 1.0 {
player.stop_mining().await;
let broken_state = world.get_block_state(&location);
let can_harvest = player.can_harvest(broken_state, block).await;
let new_state = world
.break_block(
&location,
Some(player.clone()),
BlockFlags::NOTIFY_NEIGHBORS,
)
.await;
if new_state.is_some() {
server
.block_registry
.broken(&world, block, player, &location, server, broken_state)
.await;
player.apply_tool_damage_for_block_break(broken_state).await;
if can_harvest {
player.add_exhaustion(MINE_BLOCK_EXHAUSTION).await;
}
}
} else {
let mut mining_pos = player.mining_pos.lock().await;
let starts_breaking =
!player.mining.load(Ordering::Relaxed) || *mining_pos != location;
let progress = if starts_breaking {
player.start_mining_time.store(
player.tick_counter.load(Ordering::Relaxed),
Ordering::Relaxed,
);
player.mining.store(true, Ordering::Relaxed);
*mining_pos = location;
(speed * 10.0) as i32
} else {
player.current_block_destroy_stage.load(Ordering::Relaxed)
};
drop(mining_pos);
let old_speed = player
.current_block_breaking_speed
.swap(speed.to_bits(), Ordering::Relaxed);
if starts_breaking {
world
.set_block_breaking(
entity,
location,
BlockBreakingProgress::Start {
stage: progress,
speed,
},
)
.await;
player
.current_block_destroy_stage
.store(progress, Ordering::Relaxed);
} else if old_speed != speed.to_bits() {
world
.set_block_breaking(
entity,
location,
BlockBreakingProgress::Update {
stage: progress,
speed: Some(speed),
},
)
.await;
}
}
}
}
action @ (PlayerAction::PredictDestroyBlock | PlayerAction::StopBreak) => {
let location = packet.block_pos;
if !player.can_interact_with_block_at(&location, 1.0) {
return;
}
let entity = &player.get_entity();
let world = entity.world.load_full();
let (block, state) = world.get_block_and_state(&location);
if player.gamemode.load() != GameMode::Creative && !state.is_air() {
let speed = crate::block::calc_block_breaking(player, state, block).await;
let elapsed = player.tick_counter.load(Ordering::Relaxed)
- player.start_mining_time.load(Ordering::Relaxed)
+ 1;
let same_block = *player.mining_pos.lock().await == location;
if player.mining.load(Ordering::Relaxed)
&& same_block
&& speed * elapsed as f32 >= MIN_PREDICTED_BREAK_PROGRESS
{
player.stop_mining().await;
let can_harvest = player.can_harvest(state, block).await;
let flags = if can_harvest {
BlockFlags::NOTIFY_NEIGHBORS
} else {
BlockFlags::SKIP_DROPS | BlockFlags::NOTIFY_NEIGHBORS
};
if world
.break_block(&location, Some(player.clone()), flags)
.await
.is_some()
{
server
.block_registry
.broken(&world, block, player, &location, server, state)
.await;
player.apply_tool_damage_for_block_break(state).await;
if can_harvest {
player.add_exhaustion(MINE_BLOCK_EXHAUSTION).await;
}
}
} else {
let runtime_id = pumpkin_data::BlockState::to_be_network_id(state.id);
self.enqueue_packet(&CUpdateBlock::new(location, runtime_id as u32))
.await;
if matches!(action, PlayerAction::StopBreak) {
player.stop_mining().await;
} else {
world
.set_block_breaking(
entity,
location,
BlockBreakingProgress::Update {
stage: player
.current_block_destroy_stage
.load(Ordering::Relaxed),
speed: Some(speed),
},
)
.await;
}
}
} else if matches!(action, PlayerAction::StopBreak) {
player.stop_mining().await;
}
}
PlayerAction::CrackBreak => {
// Don't do anything for this action. It is no longer used. Block
// cracking is done fully server-side.
}
PlayerAction::AbortBreak => {
player.stop_mining().await;
}
PlayerAction::DropItem => {
player.drop_held_item(false).await;
}
PlayerAction::Respawn
if player.living_entity.dead.load(Ordering::Relaxed)
|| player.living_entity.health.load() <= 0.0 =>
{
player.world().respawn_player(player, false).await;
}
// TODO
_ => {}
}
}
}

View File

@ -0,0 +1,208 @@
#[allow(clippy::wildcard_imports)]
use super::*;
impl BedrockClient {
#[expect(clippy::too_many_lines)]
pub async fn handle_player_auth_input(
&self,
player: &Arc<Player>,
packet: SPlayerAuthInput,
server: &Arc<Server>,
) {
if !player.has_client_loaded() {
return;
}
if player.living_entity.dead.load(Ordering::Relaxed)
|| player.living_entity.health.load() <= 0.0
{
return;
}
let entity = player.get_entity();
let on_ground = packet.input_data.get(InputData::VerticalCollision as usize)
&& packet.delta.y < 0.0
&& !entity.has_vehicle().await;
entity.on_ground.store(on_ground, Ordering::Relaxed);
let new_pos = packet
.position
.add_raw(0.0, -entity.entity_type.eye_height, 0.0)
.to_f64();
let old_pos = player.position();
let new_pitch = packet.pitch;
let new_yaw = packet.yaw;
let old_pitch = entity.pitch.load();
let old_yaw = entity.yaw.load();
let pos_changed = new_pos != old_pos;
let rot_changed = new_pitch != old_pitch || new_yaw != old_yaw;
if pos_changed || rot_changed {
let world = player.world();
if pos_changed {
player.get_entity().set_pos(new_pos);
}
if rot_changed {
entity.pitch.store(new_pitch);
entity.yaw.store(new_yaw);
}
let je_yaw = (new_yaw * 256.0 / 360.0).rem_euclid(256.0);
let je_pitch = (new_pitch * 256.0 / 360.0).rem_euclid(256.0);
let delta = pumpkin_util::math::vector3::Vector3::new(
new_pos.x - old_pos.x,
new_pos.y - old_pos.y,
new_pos.z - old_pos.z,
);
let bedrock_move_packet = pumpkin_protocol::bedrock::client::CMovePlayer::new(
pumpkin_protocol::codec::var_ulong::VarULong(player.entity_id() as u64),
pumpkin_util::math::vector3::Vector3::new(
new_pos.x as f32,
new_pos.y as f32 + entity.entity_type.eye_height,
new_pos.z as f32,
),
new_pitch,
new_yaw,
new_yaw, // Head yaw
pumpkin_protocol::bedrock::client::CMovePlayer::MODE_NORMAL,
on_ground,
pumpkin_protocol::codec::var_ulong::VarULong(0),
0,
0,
pumpkin_protocol::codec::var_ulong::VarULong(0),
);
if pos_changed && delta.length_squared() >= 64.0 {
world.broadcast_packet_except(
&[player.gameprofile.id],
&pumpkin_protocol::java::client::play::CEntityPositionSync::new(
player.entity_id().into(),
new_pos,
pumpkin_util::math::vector3::Vector3::new(0.0, 0.0, 0.0),
je_yaw,
je_pitch,
on_ground,
),
);
} else if pos_changed && rot_changed {
world.broadcast_packet_except_editioned_sync(
&[player.gameprofile.id],
&pumpkin_protocol::java::client::play::CUpdateEntityPosRot::new(
player.entity_id().into(),
pumpkin_util::math::vector3::Vector3::new(
new_pos.x.mul_add(4096.0, -(old_pos.x * 4096.0)) as i16,
new_pos.y.mul_add(4096.0, -(old_pos.y * 4096.0)) as i16,
new_pos.z.mul_add(4096.0, -(old_pos.z * 4096.0)) as i16,
),
je_yaw as u8, // Use converted Java byte
je_pitch as u8, // Use converted Java byte
on_ground,
),
&bedrock_move_packet,
);
} else if pos_changed {
world.broadcast_packet_except_editioned_sync(
&[player.gameprofile.id],
&pumpkin_protocol::java::client::play::CUpdateEntityPos::new(
player.entity_id().into(),
pumpkin_util::math::vector3::Vector3::new(
new_pos.x.mul_add(4096.0, -(old_pos.x * 4096.0)) as i16,
new_pos.y.mul_add(4096.0, -(old_pos.y * 4096.0)) as i16,
new_pos.z.mul_add(4096.0, -(old_pos.z * 4096.0)) as i16,
),
on_ground,
),
&bedrock_move_packet,
);
} else if rot_changed {
world.broadcast_packet_except_editioned_sync(
&[player.gameprofile.id],
&pumpkin_protocol::java::client::play::CUpdateEntityRot::new(
player.entity_id().into(),
je_yaw as u8, // Use converted Java byte
je_pitch as u8, // Use converted Java byte
on_ground,
),
&bedrock_move_packet,
);
}
if rot_changed {
world.broadcast_packet_except(
&[player.gameprofile.id],
// Adjust to `CHeadRot` if that is what your crate currently calls it
&pumpkin_protocol::java::client::play::CHeadRot::new(
player.entity_id().into(),
je_yaw as u8,
),
);
}
if pos_changed {
chunker::update_position(player).await;
player.progress_motion(delta).await;
}
}
let input_data = packet.input_data;
if input_data.get(InputData::StartSprinting as usize) {
entity.set_sprinting(true).await;
} else if input_data.get(InputData::StopSprinting as usize) {
entity.set_sprinting(false).await;
}
if input_data.get(InputData::StartSneaking as usize) {
entity.set_sneaking(true).await;
} else if input_data.get(InputData::StopSneaking as usize) {
entity.set_sneaking(false).await;
}
if input_data.get(InputData::StartFlying as usize) {
let flying = { player.abilities.lock().await.flying };
if !flying {
send_cancellable! {{
server;
PlayerToggleFlightEvent::new(player.clone(), true);
'after: {
{
player.abilities.lock().await.flying = true;
};
player.send_abilities_update().await;
}
'cancelled: {
player.send_abilities_update().await;
}
}}
}
} else if input_data.get(InputData::StopFlying as usize) {
let flying = { player.abilities.lock().await.flying };
if flying {
send_cancellable! {{
server;
PlayerToggleFlightEvent::new(player.clone(), false);
'after: {
{
player.abilities.lock().await.flying = false;
};
player.send_abilities_update().await;
}
'cancelled: {
player.send_abilities_update().await;
}
}}
}
}
if let Some(block_actions) = packet.block_actions {
for action in block_actions {
self.handle_player_block_action(player, server, action)
.await;
}
}
}
}

View File

@ -0,0 +1,28 @@
#[allow(clippy::wildcard_imports)]
use super::*;
impl BedrockClient {
pub async fn handle_player_block_action(
&self,
player: &Arc<Player>,
server: &Server,
packet: pumpkin_protocol::bedrock::server::player_auth_input::PlayerBlockAction,
) {
use pumpkin_protocol::bedrock::server::player_action::Action as PlayerAction;
let Ok(action) = PlayerAction::try_from(packet.action.0) else {
return;
};
self.handle_player_action(
player,
server,
SPlayerAction {
runtime_id: VarULong(0), // Unused
action,
block_pos: packet.block_pos,
result_pos: BlockPos::ZERO,
face: packet.face,
},
)
.await;
}
}

View File

@ -0,0 +1,34 @@
#[allow(clippy::wildcard_imports)]
use super::*;
impl BedrockClient {
pub async fn handle_request_ability(
&self,
player: &Arc<Player>,
packet: pumpkin_protocol::bedrock::server::request_ability::SRequestAbility,
) {
player.update_last_action_time();
let ability_id = packet.ability.0;
match ability_id {
9 => {
// Flying
if let pumpkin_protocol::bedrock::server::request_ability::AbilityValue::Bool(
requested_flying,
) = packet.value
{
let mut abilities = player.abilities.lock().await;
if abilities.allow_flying {
abilities.flying = requested_flying;
} else {
abilities.flying = false;
}
drop(abilities);
player.send_abilities_update().await;
}
}
_ => {
debug!("Received RequestAbility packet for unhandled ability {ability_id}");
}
}
}
}

View File

@ -0,0 +1,51 @@
#[allow(clippy::wildcard_imports)]
use super::*;
impl BedrockClient {
pub async fn handle_request_chunk_radius(
&self,
player: &Arc<Player>,
packet: SRequestChunkRadius,
) {
let chunk_radius = packet.chunk_radius;
if chunk_radius.0 < 1 {
self.kick(
DisconnectReason::Kicked,
"Cannot have zero or negative view distance!".to_string(),
)
.await;
return;
}
let Some(server) = player.world().server.upgrade() else {
return;
};
let view_distance = chunk_radius.clamp(
2,
NonZeroI32::from(server.advanced_config.networking.bedrock.view_distance).get(),
);
self.enqueue_packet(&CChunkRadiusUpdate {
chunk_radius: VarInt(view_distance),
})
.await;
let old_view_distance = {
let current_config = player.config.load();
let old_vd = current_config.view_distance;
let mut new_config = (**current_config).clone();
new_config.view_distance =
NonZero::new(view_distance as u8).unwrap_or(NonZero::<u8>::MIN);
player.config.store(std::sync::Arc::new(new_config));
old_vd
};
debug!(
"Player {} updated their render distance: {} -> {}.",
player.gameprofile.name, old_view_distance, view_distance
);
chunker::update_position(player).await;
}
}

View File

@ -0,0 +1,26 @@
#[allow(clippy::wildcard_imports)]
use super::*;
impl BedrockClient {
pub async fn handle_respawn(&self, player: &Arc<Player>, packet: SRespawn) {
if packet.state != RespawnState::ClientReadyToSpawn
|| (!player.living_entity.dead.load(Ordering::Relaxed)
&& player.living_entity.health.load() > 0.0)
{
return;
}
let entity = player.get_entity();
let position = entity.pos.load();
self.enqueue_packet(&CRespawn::new(
pumpkin_util::math::vector3::Vector3::new(
position.x as f32,
position.y as f32 + entity.entity_type.eye_height,
position.z as f32,
),
RespawnState::ReadyToSpawn,
VarULong(player.entity_id() as u64),
))
.await;
}
}

View File

@ -0,0 +1,17 @@
#[allow(clippy::wildcard_imports)]
use super::*;
impl BedrockClient {
pub fn handle_set_local_player_as_initialized(
&self,
player: &Arc<Player>,
packet: &SSetLocalPlayerAsInitialized,
) {
debug!(
"Player {} initialized (Runtime ID: {})",
player.gameprofile.name, packet.runtime_entity_id.0
);
// This is sent when the client has finished loading and rendering the world.
player.set_client_loaded(true);
}
}

View File

@ -1,309 +0,0 @@
use std::{num::NonZeroU8, sync::Arc, sync::atomic::Ordering};
use crate::{
entity::player::ChatMode,
net::{
PlayerConfig, can_not_join,
java::{JavaClient, PacketHandlerResult},
},
server::Server,
};
use core::str;
use pumpkin_data::{registry::Registry, translation};
use pumpkin_protocol::{
ConnectionState, KnownPack,
java::{
client::config::{CFeatureFlags, CFinishConfig, CKnownPacks, CRegistryData, CUpdateTags},
server::config::{
ResourcePackResponseResult, SClientInformationConfig, SConfigCookieResponse,
SConfigResourcePack, SKeepAlive, SKnownPacks, SPluginMessage,
},
},
};
use pumpkin_util::{Hand, text::TextComponent, version::JavaMinecraftVersion};
use tracing::{debug, trace, warn};
const BRAND_CHANNEL_PREFIX: &str = "minecraft:brand";
impl JavaClient {
pub async fn handle_client_information_config(
&self,
client_information: SClientInformationConfig<'_>,
) {
debug!("Handling client settings");
if client_information.view_distance <= 0 {
self.kick(TextComponent::text(
"Cannot have zero or negative view distance!",
))
.await;
return;
}
if let (Ok(main_hand), Ok(chat_mode)) = (
Hand::try_from(client_information.main_hand.0),
ChatMode::try_from(client_information.chat_mode.0),
) {
self.config.store(Arc::new(PlayerConfig {
locale: client_information.locale.to_string(),
// client_information.view_distance was checked above to be > 0 so compiler should optimize this out.
view_distance: NonZeroU8::new(client_information.view_distance as u8)
.unwrap_or(NonZeroU8::MIN),
chat_mode,
chat_colors: client_information.chat_colors,
skin_parts: client_information.skin_parts,
main_hand,
text_filtering: client_information.text_filtering,
server_listing: client_information.server_listing,
}));
} else {
self.kick(TextComponent::text("Invalid hand or chat type"))
.await;
}
}
pub async fn handle_plugin_message(&self, plugin_message: SPluginMessage<'_>) {
debug!("Handling plugin message");
if plugin_message.channel.starts_with(BRAND_CHANNEL_PREFIX) {
debug!("Got a client brand");
match str::from_utf8(plugin_message.data) {
Ok(brand) => self.brand.store(Arc::new(Some(brand.to_string()))),
Err(e) => self.kick(TextComponent::text(e.to_string())).await,
}
}
}
pub async fn handle_resource_pack_response(
&self,
server: &Server,
packet: SConfigResourcePack,
) {
let resource_config = &server.advanced_config.resource_pack.java;
if resource_config.enabled {
let expected_uuid =
uuid::Uuid::new_v3(&uuid::Uuid::NAMESPACE_DNS, resource_config.url.as_bytes());
if packet.uuid == expected_uuid {
match packet.response_result() {
ResourcePackResponseResult::DownloadSuccess => {
trace!(
"Client {} successfully downloaded the resource pack",
self.id
);
}
ResourcePackResponseResult::DownloadFail => {
warn!(
"Client {} failed to downloaded the resource pack. Is it available on the internet?",
self.id
);
}
ResourcePackResponseResult::Downloaded => {
trace!("Client {} already has the resource pack", self.id);
}
ResourcePackResponseResult::Accepted => {
trace!("Client {} accepted the resource pack", self.id);
// Return here to wait for the next response update
return;
}
ResourcePackResponseResult::Declined => {
trace!("Client {} declined the resource pack", self.id);
}
ResourcePackResponseResult::InvalidUrl => {
warn!(
"Client {} reported that the resource pack URL is invalid!",
self.id
);
}
ResourcePackResponseResult::ReloadFailed => {
trace!("Client {} failed to reload the resource pack", self.id);
}
ResourcePackResponseResult::Discarded => {
trace!("Client {} discarded the resource pack", self.id);
}
ResourcePackResponseResult::Unknown(result) => {
warn!(
"Client {} responded with a bad result: {}!",
self.id, result
);
}
}
} else {
warn!(
"Client {} returned a response for a resource pack we did not set!",
self.id
);
}
} else {
warn!(
"Client {} returned a response for a resource pack that was not enabled!",
self.id
);
}
self.send_known_packs().await;
}
pub async fn send_known_packs(&self) {
let version_str = self.version.load().to_string();
self.send_packet_now(&CKnownPacks::new(&[KnownPack {
namespace: "minecraft",
id: "core",
version: &version_str,
}]))
.await;
}
pub fn handle_config_cookie_response(&self, packet: &SConfigCookieResponse<'_>) {
// TODO: allow plugins to access this
debug!(
"Received cookie_response[config]: key: \"{}\", has_payload: \"{}\", payload_length: \"{:?}\"",
packet.key,
packet.has_payload,
packet.payload.as_ref().map(|p| p.len()),
);
}
pub async fn handle_known_packs(
&self,
_config_acknowledged: SKnownPacks<'_>,
server: &Server,
) -> Option<PacketHandlerResult> {
debug!("Handling known packs");
// let mut tags_to_send = Vec::new();
let version = self.version.load();
if version >= JavaMinecraftVersion::V_1_20_2 {
self.send_packet_now(&CFeatureFlags::new(&["minecraft:vanilla".to_string()]))
.await;
let registry = Registry::get_synced(version);
let mut sent_dimension_type = false;
for reg in &registry {
if reg.registry_id == "minecraft:dimension_type" {
sent_dimension_type = true;
}
self.send_packet_now(&CRegistryData::new(&reg.registry_id, &reg.registry_entries))
.await;
}
if !sent_dimension_type {
let dims = [
&pumpkin_data::dimension::Dimension::OVERWORLD,
&pumpkin_data::dimension::Dimension::OVERWORLD_CAVES,
&pumpkin_data::dimension::Dimension::THE_END,
&pumpkin_data::dimension::Dimension::THE_NETHER,
];
let dim_entries: Vec<pumpkin_data::registry::RegistryEntryData> = dims
.iter()
.map(|dim| pumpkin_data::registry::RegistryEntryData {
entry_id: dim.minecraft_name.to_string(),
data: Some(build_dimension_nbt(dim).into_boxed_slice()),
})
.collect();
self.send_packet_now(&CRegistryData::new(
&"minecraft:dimension_type".to_string(),
&dim_entries,
))
.await;
}
}
let all_keys = [
pumpkin_data::tag::RegistryKey::BannerPattern,
pumpkin_data::tag::RegistryKey::Block,
pumpkin_data::tag::RegistryKey::CatVariant,
pumpkin_data::tag::RegistryKey::DamageType,
pumpkin_data::tag::RegistryKey::Dialog,
pumpkin_data::tag::RegistryKey::DimensionType,
pumpkin_data::tag::RegistryKey::Enchantment,
pumpkin_data::tag::RegistryKey::EntityType,
pumpkin_data::tag::RegistryKey::Fluid,
pumpkin_data::tag::RegistryKey::GameEvent,
pumpkin_data::tag::RegistryKey::Instrument,
pumpkin_data::tag::RegistryKey::Item,
pumpkin_data::tag::RegistryKey::PaintingVariant,
pumpkin_data::tag::RegistryKey::PointOfInterestType,
pumpkin_data::tag::RegistryKey::Potion,
pumpkin_data::tag::RegistryKey::Timeline,
pumpkin_data::tag::RegistryKey::WorldgenBiome,
];
let mut tags = Vec::new();
for key in all_keys {
if pumpkin_data::tag::get_registry_key_tags(version, key)
.is_some_and(|map| !map.is_empty())
{
tags.push(key);
}
}
self.send_packet_now(&CUpdateTags::new(&tags)).await;
// We are done with configuring
self.send_packet_now(&CFinishConfig).await;
if version < JavaMinecraftVersion::V_1_20_2 {
return Some(self.handle_config_acknowledged(server).await);
}
debug!("Finished config");
None
}
pub async fn handle_config_keep_alive(&self, keep_alive: SKeepAlive) {
if self.wait_for_keep_alive.load(Ordering::Relaxed)
&& keep_alive.keep_alive_id == self.keep_alive_id.load()
{
self.wait_for_keep_alive.store(false, Ordering::Relaxed);
} else {
self.kick(pumpkin_macros::translate_cross!(
translation::java::DISCONNECT_TIMEOUT,
translation::bedrock::DISCONNECT_TIMEOUT
))
.await;
}
}
pub async fn handle_config_acknowledged(&self, server: &Server) -> PacketHandlerResult {
debug!("Handling config acknowledgement");
self.connection_state.store(ConnectionState::Play);
let profile = self.gameprofile.clone();
let address = self.address;
if let Some(reason) = can_not_join(&profile, &address, server).await {
self.kick(reason).await;
return PacketHandlerResult::Stop;
}
let config = self.config.load();
PacketHandlerResult::ReadyToPlay(profile, (**config).clone())
}
}
fn build_dimension_nbt(dim: &pumpkin_data::dimension::Dimension) -> Vec<u8> {
let mut compound = pumpkin_nbt::compound::NbtCompound::new();
compound.put_float("ambient_light", dim.ambient_light);
compound.put_int("height", dim.height);
compound.put_int("logical_height", dim.logical_height);
compound.put_int("min_y", dim.min_y);
compound.put_string("infiniburn", dim.infiniburn.to_string());
compound.put_int(
"monster_spawn_block_light_limit",
dim.monster_spawn_block_light_limit as i32,
);
compound.put_double("coordinate_scale", dim.coordinate_scale);
compound.put_byte("has_skylight", i8::from(dim.has_skylight));
compound.put_byte("has_ceiling", i8::from(dim.has_ceiling));
compound.put_byte("ultrawarm", i8::from(dim.id == 3));
compound.put_byte("natural", i8::from(dim.id == 0 || dim.id == 1));
compound.put_byte("piglin_safe", i8::from(dim.id == 3));
compound.put_byte("respawn_anchor_works", i8::from(dim.id == 3));
compound.put_byte("bed_works", i8::from(dim.id == 0 || dim.id == 1));
compound.put_byte("has_raids", i8::from(dim.id == 0 || dim.id == 1));
compound.put_string("effects", dim.minecraft_name.to_string());
let mut monster_spawn = pumpkin_nbt::compound::NbtCompound::new();
monster_spawn.put_string("type", "minecraft:uniform".to_string());
let mut value = pumpkin_nbt::compound::NbtCompound::new();
value.put_int("min_inclusive", 0);
value.put_int("max_inclusive", 7);
monster_spawn.put_compound("value", value);
compound.put_compound("monster_spawn_light_level", monster_spawn);
pumpkin_nbt::Nbt::from(compound).write().to_vec()
}

View File

@ -0,0 +1,39 @@
#[allow(clippy::wildcard_imports)]
use super::*;
impl JavaClient {
pub async fn handle_client_information_config(
&self,
client_information: SClientInformationConfig<'_>,
) {
debug!("Handling client settings");
if client_information.view_distance <= 0 {
self.kick(TextComponent::text(
"Cannot have zero or negative view distance!",
))
.await;
return;
}
if let (Ok(main_hand), Ok(chat_mode)) = (
Hand::try_from(client_information.main_hand.0),
ChatMode::try_from(client_information.chat_mode.0),
) {
self.config.store(Arc::new(PlayerConfig {
locale: client_information.locale.to_string(),
// client_information.view_distance was checked above to be > 0 so compiler should optimize this out.
view_distance: NonZeroU8::new(client_information.view_distance as u8)
.unwrap_or(NonZeroU8::MIN),
chat_mode,
chat_colors: client_information.chat_colors,
skin_parts: client_information.skin_parts,
main_hand,
text_filtering: client_information.text_filtering,
server_listing: client_information.server_listing,
}));
} else {
self.kick(TextComponent::text("Invalid hand or chat type"))
.await;
}
}
}

View File

@ -0,0 +1,53 @@
#[allow(clippy::wildcard_imports)]
use super::*;
impl JavaClient {
pub async fn handle_config_acknowledged(&self, server: &Server) -> PacketHandlerResult {
debug!("Handling config acknowledgement");
self.connection_state.store(ConnectionState::Play);
let profile = self.gameprofile.clone();
let address = self.address;
if let Some(reason) = can_not_join(&profile, &address, server).await {
self.kick(reason).await;
return PacketHandlerResult::Stop;
}
let config = self.config.load();
PacketHandlerResult::ReadyToPlay(profile, (**config).clone())
}
}
pub(crate) fn build_dimension_nbt(dim: &pumpkin_data::dimension::Dimension) -> Vec<u8> {
let mut compound = pumpkin_nbt::compound::NbtCompound::new();
compound.put_float("ambient_light", dim.ambient_light);
compound.put_int("height", dim.height);
compound.put_int("logical_height", dim.logical_height);
compound.put_int("min_y", dim.min_y);
compound.put_string("infiniburn", dim.infiniburn.to_string());
compound.put_int(
"monster_spawn_block_light_limit",
dim.monster_spawn_block_light_limit as i32,
);
compound.put_double("coordinate_scale", dim.coordinate_scale);
compound.put_byte("has_skylight", i8::from(dim.has_skylight));
compound.put_byte("has_ceiling", i8::from(dim.has_ceiling));
compound.put_byte("ultrawarm", i8::from(dim.id == 3));
compound.put_byte("natural", i8::from(dim.id == 0 || dim.id == 1));
compound.put_byte("piglin_safe", i8::from(dim.id == 3));
compound.put_byte("respawn_anchor_works", i8::from(dim.id == 3));
compound.put_byte("bed_works", i8::from(dim.id == 0 || dim.id == 1));
compound.put_byte("has_raids", i8::from(dim.id == 0 || dim.id == 1));
compound.put_string("effects", dim.minecraft_name.to_string());
let mut monster_spawn = pumpkin_nbt::compound::NbtCompound::new();
monster_spawn.put_string("type", "minecraft:uniform".to_string());
let mut value = pumpkin_nbt::compound::NbtCompound::new();
value.put_int("min_inclusive", 0);
value.put_int("max_inclusive", 7);
monster_spawn.put_compound("value", value);
compound.put_compound("monster_spawn_light_level", monster_spawn);
pumpkin_nbt::Nbt::from(compound).write().to_vec()
}

View File

@ -0,0 +1,14 @@
#[allow(clippy::wildcard_imports)]
use super::*;
impl JavaClient {
pub fn handle_config_cookie_response(&self, packet: &SConfigCookieResponse<'_>) {
// TODO: allow plugins to access this
debug!(
"Received cookie_response[config]: key: \"{}\", has_payload: \"{}\", payload_length: \"{:?}\"",
packet.key,
packet.has_payload,
packet.payload.as_ref().map(|p| p.len()),
);
}
}

View File

@ -0,0 +1,18 @@
#[allow(clippy::wildcard_imports)]
use super::*;
impl JavaClient {
pub async fn handle_config_keep_alive(&self, keep_alive: SKeepAlive) {
if self.wait_for_keep_alive.load(Ordering::Relaxed)
&& keep_alive.keep_alive_id == self.keep_alive_id.load()
{
self.wait_for_keep_alive.store(false, Ordering::Relaxed);
} else {
self.kick(pumpkin_macros::translate_cross!(
translation::java::DISCONNECT_TIMEOUT,
translation::bedrock::DISCONNECT_TIMEOUT
))
.await;
}
}
}

View File

@ -0,0 +1,86 @@
#[allow(clippy::wildcard_imports)]
use super::*;
impl JavaClient {
pub async fn handle_known_packs(
&self,
_config_acknowledged: SKnownPacks<'_>,
server: &Server,
) -> Option<PacketHandlerResult> {
debug!("Handling known packs");
// let mut tags_to_send = Vec::new();
let version = self.version.load();
if version >= JavaMinecraftVersion::V_1_20_2 {
self.send_packet_now(&CFeatureFlags::new(&["minecraft:vanilla".to_string()]))
.await;
let registry = Registry::get_synced(version);
let mut sent_dimension_type = false;
for reg in &registry {
if reg.registry_id == "minecraft:dimension_type" {
sent_dimension_type = true;
}
self.send_packet_now(&CRegistryData::new(&reg.registry_id, &reg.registry_entries))
.await;
}
if !sent_dimension_type {
let dims = [
&pumpkin_data::dimension::Dimension::OVERWORLD,
&pumpkin_data::dimension::Dimension::OVERWORLD_CAVES,
&pumpkin_data::dimension::Dimension::THE_END,
&pumpkin_data::dimension::Dimension::THE_NETHER,
];
let dim_entries: Vec<pumpkin_data::registry::RegistryEntryData> = dims
.iter()
.map(|dim| pumpkin_data::registry::RegistryEntryData {
entry_id: dim.minecraft_name.to_string(),
data: Some(build_dimension_nbt(dim).into_boxed_slice()),
})
.collect();
self.send_packet_now(&CRegistryData::new(
&"minecraft:dimension_type".to_string(),
&dim_entries,
))
.await;
}
}
let all_keys = [
pumpkin_data::tag::RegistryKey::BannerPattern,
pumpkin_data::tag::RegistryKey::Block,
pumpkin_data::tag::RegistryKey::CatVariant,
pumpkin_data::tag::RegistryKey::DamageType,
pumpkin_data::tag::RegistryKey::Dialog,
pumpkin_data::tag::RegistryKey::DimensionType,
pumpkin_data::tag::RegistryKey::Enchantment,
pumpkin_data::tag::RegistryKey::EntityType,
pumpkin_data::tag::RegistryKey::Fluid,
pumpkin_data::tag::RegistryKey::GameEvent,
pumpkin_data::tag::RegistryKey::Instrument,
pumpkin_data::tag::RegistryKey::Item,
pumpkin_data::tag::RegistryKey::PaintingVariant,
pumpkin_data::tag::RegistryKey::PointOfInterestType,
pumpkin_data::tag::RegistryKey::Potion,
pumpkin_data::tag::RegistryKey::Timeline,
pumpkin_data::tag::RegistryKey::WorldgenBiome,
];
let mut tags = Vec::new();
for key in all_keys {
if pumpkin_data::tag::get_registry_key_tags(version, key)
.is_some_and(|map| !map.is_empty())
{
tags.push(key);
}
}
self.send_packet_now(&CUpdateTags::new(&tags)).await;
// We are done with configuring
self.send_packet_now(&CFinishConfig).await;
if version < JavaMinecraftVersion::V_1_20_2 {
return Some(self.handle_config_acknowledged(server).await);
}
debug!("Finished config");
None
}
}

View File

@ -0,0 +1,35 @@
use std::{num::NonZeroU8, sync::Arc, sync::atomic::Ordering};
use crate::{
entity::player::ChatMode,
net::{
PlayerConfig, can_not_join,
java::{JavaClient, PacketHandlerResult},
},
server::Server,
};
use core::str;
use pumpkin_data::{registry::Registry, translation};
use pumpkin_protocol::{
ConnectionState, KnownPack,
java::{
client::config::{CFeatureFlags, CFinishConfig, CKnownPacks, CRegistryData, CUpdateTags},
server::config::{
ResourcePackResponseResult, SClientInformationConfig, SConfigCookieResponse,
SConfigResourcePack, SKeepAlive, SKnownPacks, SPluginMessage,
},
},
};
use pumpkin_util::{Hand, text::TextComponent, version::JavaMinecraftVersion};
use tracing::{debug, trace, warn};
const BRAND_CHANNEL_PREFIX: &str = "minecraft:brand";
pub mod client_information;
pub mod config_acknowledged;
pub(super) use config_acknowledged::build_dimension_nbt;
pub mod cookie_response;
pub mod keep_alive;
pub mod known_packs;
pub mod plugin_message;
pub mod resource_pack;

View File

@ -0,0 +1,15 @@
#[allow(clippy::wildcard_imports)]
use super::*;
impl JavaClient {
pub async fn handle_plugin_message(&self, plugin_message: SPluginMessage<'_>) {
debug!("Handling plugin message");
if plugin_message.channel.starts_with(BRAND_CHANNEL_PREFIX) {
debug!("Got a client brand");
match str::from_utf8(plugin_message.data) {
Ok(brand) => self.brand.store(Arc::new(Some(brand.to_string()))),
Err(e) => self.kick(TextComponent::text(e.to_string())).await,
}
}
}
}

View File

@ -0,0 +1,84 @@
#[allow(clippy::wildcard_imports)]
use super::*;
impl JavaClient {
pub async fn handle_resource_pack_response(
&self,
server: &Server,
packet: SConfigResourcePack,
) {
let resource_config = &server.advanced_config.resource_pack.java;
if resource_config.enabled {
let expected_uuid =
uuid::Uuid::new_v3(&uuid::Uuid::NAMESPACE_DNS, resource_config.url.as_bytes());
if packet.uuid == expected_uuid {
match packet.response_result() {
ResourcePackResponseResult::DownloadSuccess => {
trace!(
"Client {} successfully downloaded the resource pack",
self.id
);
}
ResourcePackResponseResult::DownloadFail => {
warn!(
"Client {} failed to downloaded the resource pack. Is it available on the internet?",
self.id
);
}
ResourcePackResponseResult::Downloaded => {
trace!("Client {} already has the resource pack", self.id);
}
ResourcePackResponseResult::Accepted => {
trace!("Client {} accepted the resource pack", self.id);
// Return here to wait for the next response update
return;
}
ResourcePackResponseResult::Declined => {
trace!("Client {} declined the resource pack", self.id);
}
ResourcePackResponseResult::InvalidUrl => {
warn!(
"Client {} reported that the resource pack URL is invalid!",
self.id
);
}
ResourcePackResponseResult::ReloadFailed => {
trace!("Client {} failed to reload the resource pack", self.id);
}
ResourcePackResponseResult::Discarded => {
trace!("Client {} discarded the resource pack", self.id);
}
ResourcePackResponseResult::Unknown(result) => {
warn!(
"Client {} responded with a bad result: {}!",
self.id, result
);
}
}
} else {
warn!(
"Client {} returned a response for a resource pack we did not set!",
self.id
);
}
} else {
warn!(
"Client {} returned a response for a resource pack that was not enabled!",
self.id
);
}
self.send_known_packs().await;
}
pub async fn send_known_packs(&self) {
let version_str = self.version.load().to_string();
self.send_packet_now(&CKnownPacks::new(&[KnownPack {
namespace: "minecraft",
id: "core",
version: &version_str,
}]))
.await;
}
}

View File

@ -1,467 +0,0 @@
use arc_swap::ArcSwap;
use pumpkin_data::translation;
use pumpkin_protocol::{
ConnectionState, KnownPack, Label, Link, LinkType,
java::client::{
config::{
CConfigAddResourcePack, CConfigServerLinks, CFeatureFlags, CFinishConfig, CKnownPacks,
CRegistryData, CUpdateTags,
},
login::{CLoginSuccess, CSetCompression},
},
java::server::config::SKnownPacks,
java::server::login::{
SEncryptionResponse, SLoginCookieResponse, SLoginPluginResponse, SLoginStart,
},
};
use pumpkin_util::{text::TextComponent, version::JavaMinecraftVersion};
use std::sync::Arc;
use tracing::debug;
use uuid::Uuid;
use crate::{
net::{
GameProfile,
authentication::{self, AuthError},
is_valid_player_name,
java::pending::PendingConnection,
offline_uuid,
proxy::{bungeecord, velocity},
},
server::Server,
};
impl PendingConnection {
pub async fn handle_login_start(&mut self, server: &Server, login_start: SLoginStart) {
debug!("login start");
let max_players = server.advanced_config.networking.java.max_players;
if max_players > 0 && server.get_player_count() >= max_players as usize {
self.kick(TextComponent::translate_cross(
translation::java::MULTIPLAYER_DISCONNECT_SERVER_FULL,
translation::bedrock::DISCONNECTIONSCREEN_SERVERFULL,
[],
))
.await;
return;
}
if !is_valid_player_name(&login_start.name) {
self.kick(TextComponent::text("Invalid characters in username"))
.await;
return;
}
let proxy = &server.advanced_config.networking.proxy;
if proxy.enabled {
if proxy.velocity.enabled {
velocity::velocity_login(self).await;
} else if proxy.bungeecord.enabled {
match bungeecord::bungeecord_login(
&self.address,
&self.server_address,
login_start.name.into_string(),
) {
Ok((_ip, profile)) => {
self.finish_login(&profile).await;
self.gameprofile = Some(profile);
}
Err(error) => self.kick(TextComponent::text(error.to_string())).await,
}
}
} else {
let id = if server.advanced_config.networking.java.online_mode {
login_start.uuid
} else {
offline_uuid(&login_start.name).unwrap_or_else(|_| uuid::Uuid::nil())
};
let profile = GameProfile {
id,
name: login_start.name.into_string(),
properties: ArcSwap::new(Arc::new(vec![])),
profile_actions: None,
};
if server.advanced_config.networking.java.compression.enabled {
self.enable_compression(server).await;
}
if server.advanced_config.networking.java.encryption {
let verify_token: [u8; 4] = rand::random();
self.send_packet_now(
&server
.encryption_request(
&verify_token,
server.advanced_config.networking.java.online_mode,
)
.await,
)
.await;
} else {
self.finish_login(&profile).await;
}
self.gameprofile = Some(profile);
}
}
pub async fn handle_encryption_response(
&mut self,
server: &Server,
encryption_response: SEncryptionResponse,
) {
debug!("Handling encryption");
let Ok(shared_secret) = server.decrypt(&encryption_response.shared_secret).await else {
self.kick(TextComponent::text("Failed to decrypt shared secret"))
.await;
return;
};
if let Err(error) = self.set_encryption(&shared_secret) {
self.kick(TextComponent::text(error.to_string())).await;
return;
}
let profile_name = {
let Some(profile) = self.gameprofile.as_ref() else {
self.kick(TextComponent::text("No `GameProfile`")).await;
return;
};
profile.name.clone()
};
if server.advanced_config.networking.java.online_mode {
match self
.authenticate(server, &shared_secret, &profile_name)
.await
{
Ok(new_profile) => self.gameprofile = Some(new_profile),
Err(error) => {
self.kick(match error {
AuthError::FailedResponse => TextComponent::translate_cross(
translation::java::MULTIPLAYER_DISCONNECT_AUTHSERVERS_DOWN,
translation::bedrock::DISCONNECT_LOGINFAILEDINFO_SERVERSUNAVAILABLE,
[],
),
AuthError::UnverifiedUsername => TextComponent::translate_cross(
translation::java::MULTIPLAYER_DISCONNECT_UNVERIFIED_USERNAME,
translation::bedrock::DISCONNECT_LOGINFAILEDINFO_INVALIDSESSION,
[],
),
e => TextComponent::text(e.to_string()),
})
.await;
return;
}
}
}
let Some(profile) = self.gameprofile.clone() else {
return;
};
if let Some(online_player) = &server.get_player_by_uuid(profile.id) {
debug!(
"Player (IP '{}', username '{}') tried to log in with the same UUID ('{}') as an online player (username '{}')",
&self.address, &profile.name, &profile.id, &online_player.gameprofile.name
);
self.kick(TextComponent::translate_cross(
translation::java::MULTIPLAYER_DISCONNECT_DUPLICATE_LOGIN,
translation::bedrock::DISCONNECTIONSCREEN_LOGGEDINOTHERLOCATION,
[],
))
.await;
return;
}
if let Some(online_player) = &server.get_player_by_name(&profile.name) {
debug!(
"A player (IP '{}', attempted username '{}') tried to log in with the same username as an online player (UUID '{}', username '{}')",
&self.address, &profile.name, &profile.id, &online_player.gameprofile.name
);
self.kick(TextComponent::translate_cross(
translation::java::MULTIPLAYER_DISCONNECT_DUPLICATE_LOGIN,
translation::bedrock::DISCONNECTIONSCREEN_LOGGEDINOTHERLOCATION,
[],
))
.await;
return;
}
self.finish_login(&profile).await;
}
async fn enable_compression(&mut self, server: &Server) {
if self.version.load() < JavaMinecraftVersion::V_1_8 {
return;
}
let compression = server
.advanced_config
.networking
.java
.compression
.info
.clone();
self.send_packet_now(&CSetCompression::new(
pumpkin_protocol::codec::var_int::VarInt(compression.threshold as i32),
))
.await;
self.set_compression(&compression);
}
async fn finish_login(&mut self, profile: &GameProfile) {
let props = profile.properties.load();
let packet = CLoginSuccess::new(
&profile.id,
&profile.name,
&props,
false,
uuid::Uuid::new_v4(),
);
self.send_packet_now(&packet).await;
if self.version.load() < JavaMinecraftVersion::V_1_20_2 {
self.connection_state.store(ConnectionState::Play);
}
}
async fn authenticate(
&self,
server: &Server,
shared_secret: &[u8],
username: &str,
) -> Result<GameProfile, AuthError> {
let hash = server.digest_secret(shared_secret).await;
let ip = self.address.ip();
let profile = authentication::authenticate(
username,
&hash,
&ip,
&server.advanced_config.networking.java.authentication,
)?;
if let Some(actions) = &profile.profile_actions {
if server
.advanced_config
.networking
.java
.authentication
.player_profile
.allow_banned_players
{
for allowed in &server
.advanced_config
.networking
.java
.authentication
.player_profile
.allowed_actions
{
if !actions.contains(allowed) {
return Err(AuthError::DisallowedAction);
}
}
if !actions.is_empty() {
return Err(AuthError::Banned);
}
} else if !actions.is_empty() {
return Err(AuthError::Banned);
}
}
for property in profile.properties.load().iter() {
authentication::validate_textures(
property,
&server
.advanced_config
.networking
.java
.authentication
.textures,
)
.map_err(AuthError::TextureError)?;
}
Ok(profile)
}
pub fn handle_login_cookie_response(&self, packet: &SLoginCookieResponse<'_>) {
debug!(
"Received cookie_response[login]: key: \"{}\", payload_length: \"{:?}\"",
packet.key,
packet.payload.as_ref().map(|p| p.len())
);
}
pub async fn handle_plugin_response(
&mut self,
server: &Server,
plugin_response: SLoginPluginResponse,
) {
debug!("Handling plugin");
let velocity_config = &server.advanced_config.networking.proxy.velocity;
if velocity_config.enabled {
match velocity::receive_velocity_plugin_response(
self.address.port(),
velocity_config,
plugin_response,
) {
Ok((profile, new_address)) => {
self.finish_login(&profile).await;
self.gameprofile = Some(profile);
self.address = new_address;
}
Err(error) => self.kick(TextComponent::text(error.to_string())).await,
}
}
}
pub async fn handle_login_acknowledged(&mut self, server: &Server) {
debug!("Handling login acknowledgement");
self.connection_state.store(ConnectionState::Config);
self.send_packet_now(&server.get_branding()).await;
if server.advanced_config.server_links.enabled
&& self.version.load() >= JavaMinecraftVersion::V_1_21
{
let mut links: Vec<Link> = Vec::new();
let bug_report = &server.advanced_config.server_links.bug_report;
if !bug_report.is_empty() {
links.push(Link::new(Label::BuiltIn(LinkType::BugReport), bug_report));
}
let support = &server.advanced_config.server_links.support;
if !support.is_empty() {
links.push(Link::new(Label::BuiltIn(LinkType::Support), support));
}
let status = &server.advanced_config.server_links.status;
if !status.is_empty() {
links.push(Link::new(Label::BuiltIn(LinkType::Status), status));
}
let feedback = &server.advanced_config.server_links.feedback;
if !feedback.is_empty() {
links.push(Link::new(Label::BuiltIn(LinkType::Feedback), feedback));
}
let community = &server.advanced_config.server_links.community;
if !community.is_empty() {
links.push(Link::new(Label::BuiltIn(LinkType::Community), community));
}
let website = &server.advanced_config.server_links.website;
if !website.is_empty() {
links.push(Link::new(Label::BuiltIn(LinkType::Website), website));
}
let forums = &server.advanced_config.server_links.forums;
if !forums.is_empty() {
links.push(Link::new(Label::BuiltIn(LinkType::Forums), forums));
}
let news = &server.advanced_config.server_links.news;
if !news.is_empty() {
links.push(Link::new(Label::BuiltIn(LinkType::News), news));
}
let announcements = &server.advanced_config.server_links.announcements;
if !announcements.is_empty() {
links.push(Link::new(
Label::BuiltIn(LinkType::Announcements),
announcements,
));
}
for (key, value) in &server.advanced_config.server_links.custom {
links.push(Link::new(
Label::TextComponent(TextComponent::text(key.clone()).into()),
value,
));
}
self.send_packet_now(&CConfigServerLinks::new(&links)).await;
}
let resource_config = &server.advanced_config.resource_pack.java;
if resource_config.enabled {
let uuid = Uuid::new_v3(&uuid::Uuid::NAMESPACE_DNS, resource_config.url.as_bytes());
let resource_pack = CConfigAddResourcePack::new(
&uuid,
&resource_config.url,
&resource_config.sha1,
resource_config.force,
if resource_config.prompt_message.is_empty() {
None
} else {
Some(TextComponent::text(resource_config.prompt_message.clone()))
},
);
self.send_packet_now(&resource_pack).await;
} else if self.version.load() >= JavaMinecraftVersion::V_1_20_5 {
self.send_known_packs().await;
} else {
self.handle_known_packs(
SKnownPacks {
known_packs: Vec::new(),
},
server,
)
.await;
}
debug!("login acknowledged");
}
pub async fn send_known_packs(&mut self) {
let version_str = self.version.load().to_string();
self.send_packet_now(&CKnownPacks::new(&[KnownPack {
namespace: "minecraft",
id: "core",
version: &version_str,
}]))
.await;
}
pub async fn handle_known_packs(&mut self, _packet: SKnownPacks<'_>, _server: &Server) {
let version = self.version.load();
if version >= JavaMinecraftVersion::V_1_20_2 {
self.send_packet_now(&CFeatureFlags::new(&["minecraft:vanilla".to_string()]))
.await;
let registry = pumpkin_data::registry::Registry::get_synced(version);
for reg in &registry {
self.send_packet_now(&CRegistryData::new(&reg.registry_id, &reg.registry_entries))
.await;
}
}
let all_keys = [
pumpkin_data::tag::RegistryKey::BannerPattern,
pumpkin_data::tag::RegistryKey::Block,
pumpkin_data::tag::RegistryKey::CatVariant,
pumpkin_data::tag::RegistryKey::DamageType,
pumpkin_data::tag::RegistryKey::Dialog,
pumpkin_data::tag::RegistryKey::DimensionType,
pumpkin_data::tag::RegistryKey::Enchantment,
pumpkin_data::tag::RegistryKey::EntityType,
pumpkin_data::tag::RegistryKey::Fluid,
pumpkin_data::tag::RegistryKey::GameEvent,
pumpkin_data::tag::RegistryKey::Instrument,
pumpkin_data::tag::RegistryKey::Item,
pumpkin_data::tag::RegistryKey::PaintingVariant,
pumpkin_data::tag::RegistryKey::PointOfInterestType,
pumpkin_data::tag::RegistryKey::Potion,
pumpkin_data::tag::RegistryKey::Timeline,
pumpkin_data::tag::RegistryKey::WorldgenBiome,
];
let mut tags = Vec::new();
for key in all_keys {
if pumpkin_data::tag::get_registry_key_tags(version, key)
.is_some_and(|map| !map.is_empty())
{
tags.push(key);
}
}
self.send_packet_now(&CUpdateTags::new(&tags)).await;
self.send_packet_now(&CFinishConfig).await;
}
}

View File

@ -0,0 +1,12 @@
#[allow(clippy::wildcard_imports)]
use super::*;
impl PendingConnection {
pub fn handle_login_cookie_response(&self, packet: &SLoginCookieResponse<'_>) {
debug!(
"Received cookie_response[login]: key: \"{}\", payload_length: \"{:?}\"",
packet.key,
packet.payload.as_ref().map(|p| p.len())
);
}
}

View File

@ -0,0 +1,181 @@
#[allow(clippy::wildcard_imports)]
use super::*;
impl PendingConnection {
pub async fn handle_encryption_response(
&mut self,
server: &Server,
encryption_response: SEncryptionResponse,
) {
debug!("Handling encryption");
let Ok(shared_secret) = server.decrypt(&encryption_response.shared_secret).await else {
self.kick(TextComponent::text("Failed to decrypt shared secret"))
.await;
return;
};
if let Err(error) = self.set_encryption(&shared_secret) {
self.kick(TextComponent::text(error.to_string())).await;
return;
}
let profile_name = {
let Some(profile) = self.gameprofile.as_ref() else {
self.kick(TextComponent::text("No `GameProfile`")).await;
return;
};
profile.name.clone()
};
if server.advanced_config.networking.java.online_mode {
match self
.authenticate(server, &shared_secret, &profile_name)
.await
{
Ok(new_profile) => self.gameprofile = Some(new_profile),
Err(error) => {
self.kick(match error {
AuthError::FailedResponse => TextComponent::translate_cross(
translation::java::MULTIPLAYER_DISCONNECT_AUTHSERVERS_DOWN,
translation::bedrock::DISCONNECT_LOGINFAILEDINFO_SERVERSUNAVAILABLE,
[],
),
AuthError::UnverifiedUsername => TextComponent::translate_cross(
translation::java::MULTIPLAYER_DISCONNECT_UNVERIFIED_USERNAME,
translation::bedrock::DISCONNECT_LOGINFAILEDINFO_INVALIDSESSION,
[],
),
e => TextComponent::text(e.to_string()),
})
.await;
return;
}
}
}
let Some(profile) = self.gameprofile.clone() else {
return;
};
if let Some(online_player) = &server.get_player_by_uuid(profile.id) {
debug!(
"Player (IP '{}', username '{}') tried to log in with the same UUID ('{}') as an online player (username '{}')",
&self.address, &profile.name, &profile.id, &online_player.gameprofile.name
);
self.kick(TextComponent::translate_cross(
translation::java::MULTIPLAYER_DISCONNECT_DUPLICATE_LOGIN,
translation::bedrock::DISCONNECTIONSCREEN_LOGGEDINOTHERLOCATION,
[],
))
.await;
return;
}
if let Some(online_player) = &server.get_player_by_name(&profile.name) {
debug!(
"A player (IP '{}', attempted username '{}') tried to log in with the same username as an online player (UUID '{}', username '{}')",
&self.address, &profile.name, &profile.id, &online_player.gameprofile.name
);
self.kick(TextComponent::translate_cross(
translation::java::MULTIPLAYER_DISCONNECT_DUPLICATE_LOGIN,
translation::bedrock::DISCONNECTIONSCREEN_LOGGEDINOTHERLOCATION,
[],
))
.await;
return;
}
self.finish_login(&profile).await;
}
pub(super) async fn enable_compression(&mut self, server: &Server) {
if self.version.load() < JavaMinecraftVersion::V_1_8 {
return;
}
let compression = server
.advanced_config
.networking
.java
.compression
.info
.clone();
self.send_packet_now(&CSetCompression::new(
pumpkin_protocol::codec::var_int::VarInt(compression.threshold as i32),
))
.await;
self.set_compression(&compression);
}
pub(super) async fn finish_login(&mut self, profile: &GameProfile) {
let props = profile.properties.load();
let packet = CLoginSuccess::new(
&profile.id,
&profile.name,
&props,
false,
uuid::Uuid::new_v4(),
);
self.send_packet_now(&packet).await;
if self.version.load() < JavaMinecraftVersion::V_1_20_2 {
self.connection_state.store(ConnectionState::Play);
}
}
async fn authenticate(
&self,
server: &Server,
shared_secret: &[u8],
username: &str,
) -> Result<GameProfile, AuthError> {
let hash = server.digest_secret(shared_secret).await;
let ip = self.address.ip();
let profile = authentication::authenticate(
username,
&hash,
&ip,
&server.advanced_config.networking.java.authentication,
)?;
if let Some(actions) = &profile.profile_actions {
if server
.advanced_config
.networking
.java
.authentication
.player_profile
.allow_banned_players
{
for allowed in &server
.advanced_config
.networking
.java
.authentication
.player_profile
.allowed_actions
{
if !actions.contains(allowed) {
return Err(AuthError::DisallowedAction);
}
}
if !actions.is_empty() {
return Err(AuthError::Banned);
}
} else if !actions.is_empty() {
return Err(AuthError::Banned);
}
}
for property in profile.properties.load().iter() {
authentication::validate_textures(
property,
&server
.advanced_config
.networking
.java
.authentication
.textures,
)
.map_err(AuthError::TextureError)?;
}
Ok(profile)
}
}

View File

@ -0,0 +1,47 @@
#[allow(clippy::wildcard_imports)]
use super::*;
impl PendingConnection {
pub async fn handle_known_packs(&mut self, _packet: SKnownPacks<'_>, _server: &Server) {
let version = self.version.load();
if version >= JavaMinecraftVersion::V_1_20_2 {
self.send_packet_now(&CFeatureFlags::new(&["minecraft:vanilla".to_string()]))
.await;
let registry = pumpkin_data::registry::Registry::get_synced(version);
for reg in &registry {
self.send_packet_now(&CRegistryData::new(&reg.registry_id, &reg.registry_entries))
.await;
}
}
let all_keys = [
pumpkin_data::tag::RegistryKey::BannerPattern,
pumpkin_data::tag::RegistryKey::Block,
pumpkin_data::tag::RegistryKey::CatVariant,
pumpkin_data::tag::RegistryKey::DamageType,
pumpkin_data::tag::RegistryKey::Dialog,
pumpkin_data::tag::RegistryKey::DimensionType,
pumpkin_data::tag::RegistryKey::Enchantment,
pumpkin_data::tag::RegistryKey::EntityType,
pumpkin_data::tag::RegistryKey::Fluid,
pumpkin_data::tag::RegistryKey::GameEvent,
pumpkin_data::tag::RegistryKey::Instrument,
pumpkin_data::tag::RegistryKey::Item,
pumpkin_data::tag::RegistryKey::PaintingVariant,
pumpkin_data::tag::RegistryKey::PointOfInterestType,
pumpkin_data::tag::RegistryKey::Potion,
pumpkin_data::tag::RegistryKey::Timeline,
pumpkin_data::tag::RegistryKey::WorldgenBiome,
];
let mut tags = Vec::new();
for key in all_keys {
if pumpkin_data::tag::get_registry_key_tags(version, key)
.is_some_and(|map| !map.is_empty())
{
tags.push(key);
}
}
self.send_packet_now(&CUpdateTags::new(&tags)).await;
self.send_packet_now(&CFinishConfig).await;
}
}

View File

@ -0,0 +1,112 @@
#[allow(clippy::wildcard_imports)]
use super::*;
impl PendingConnection {
pub async fn handle_login_acknowledged(&mut self, server: &Server) {
debug!("Handling login acknowledgement");
self.connection_state.store(ConnectionState::Config);
self.send_packet_now(&server.get_branding()).await;
if server.advanced_config.server_links.enabled
&& self.version.load() >= JavaMinecraftVersion::V_1_21
{
let mut links: Vec<Link> = Vec::new();
let bug_report = &server.advanced_config.server_links.bug_report;
if !bug_report.is_empty() {
links.push(Link::new(Label::BuiltIn(LinkType::BugReport), bug_report));
}
let support = &server.advanced_config.server_links.support;
if !support.is_empty() {
links.push(Link::new(Label::BuiltIn(LinkType::Support), support));
}
let status = &server.advanced_config.server_links.status;
if !status.is_empty() {
links.push(Link::new(Label::BuiltIn(LinkType::Status), status));
}
let feedback = &server.advanced_config.server_links.feedback;
if !feedback.is_empty() {
links.push(Link::new(Label::BuiltIn(LinkType::Feedback), feedback));
}
let community = &server.advanced_config.server_links.community;
if !community.is_empty() {
links.push(Link::new(Label::BuiltIn(LinkType::Community), community));
}
let website = &server.advanced_config.server_links.website;
if !website.is_empty() {
links.push(Link::new(Label::BuiltIn(LinkType::Website), website));
}
let forums = &server.advanced_config.server_links.forums;
if !forums.is_empty() {
links.push(Link::new(Label::BuiltIn(LinkType::Forums), forums));
}
let news = &server.advanced_config.server_links.news;
if !news.is_empty() {
links.push(Link::new(Label::BuiltIn(LinkType::News), news));
}
let announcements = &server.advanced_config.server_links.announcements;
if !announcements.is_empty() {
links.push(Link::new(
Label::BuiltIn(LinkType::Announcements),
announcements,
));
}
for (key, value) in &server.advanced_config.server_links.custom {
links.push(Link::new(
Label::TextComponent(TextComponent::text(key.clone()).into()),
value,
));
}
self.send_packet_now(&CConfigServerLinks::new(&links)).await;
}
let resource_config = &server.advanced_config.resource_pack.java;
if resource_config.enabled {
let uuid = Uuid::new_v3(&uuid::Uuid::NAMESPACE_DNS, resource_config.url.as_bytes());
let resource_pack = CConfigAddResourcePack::new(
&uuid,
&resource_config.url,
&resource_config.sha1,
resource_config.force,
if resource_config.prompt_message.is_empty() {
None
} else {
Some(TextComponent::text(resource_config.prompt_message.clone()))
},
);
self.send_packet_now(&resource_pack).await;
} else if self.version.load() >= JavaMinecraftVersion::V_1_20_5 {
self.send_known_packs().await;
} else {
self.handle_known_packs(
SKnownPacks {
known_packs: Vec::new(),
},
server,
)
.await;
}
debug!("login acknowledged");
}
pub async fn send_known_packs(&mut self) {
let version_str = self.version.load().to_string();
self.send_packet_now(&CKnownPacks::new(&[KnownPack {
namespace: "minecraft",
id: "core",
version: &version_str,
}]))
.await;
}
}

View File

@ -0,0 +1,78 @@
#[allow(clippy::wildcard_imports)]
use super::*;
impl PendingConnection {
pub async fn handle_login_start(&mut self, server: &Server, login_start: SLoginStart) {
debug!("login start");
let max_players = server.advanced_config.networking.java.max_players;
if max_players > 0 && server.get_player_count() >= max_players as usize {
self.kick(TextComponent::translate_cross(
translation::java::MULTIPLAYER_DISCONNECT_SERVER_FULL,
translation::bedrock::DISCONNECTIONSCREEN_SERVERFULL,
[],
))
.await;
return;
}
if !is_valid_player_name(&login_start.name) {
self.kick(TextComponent::text("Invalid characters in username"))
.await;
return;
}
let proxy = &server.advanced_config.networking.proxy;
if proxy.enabled {
if proxy.velocity.enabled {
velocity::velocity_login(self).await;
} else if proxy.bungeecord.enabled {
match bungeecord::bungeecord_login(
&self.address,
&self.server_address,
login_start.name.into_string(),
) {
Ok((_ip, profile)) => {
self.finish_login(&profile).await;
self.gameprofile = Some(profile);
}
Err(error) => self.kick(TextComponent::text(error.to_string())).await,
}
}
} else {
let id = if server.advanced_config.networking.java.online_mode {
login_start.uuid
} else {
offline_uuid(&login_start.name).unwrap_or_else(|_| uuid::Uuid::nil())
};
let profile = GameProfile {
id,
name: login_start.name.into_string(),
properties: ArcSwap::new(Arc::new(vec![])),
profile_actions: None,
};
if server.advanced_config.networking.java.compression.enabled {
self.enable_compression(server).await;
}
if server.advanced_config.networking.java.encryption {
let verify_token: [u8; 4] = rand::random();
self.send_packet_now(
&server
.encryption_request(
&verify_token,
server.advanced_config.networking.java.online_mode,
)
.await,
)
.await;
} else {
self.finish_login(&profile).await;
}
self.gameprofile = Some(profile);
}
}
}

View File

@ -0,0 +1,39 @@
use arc_swap::ArcSwap;
use pumpkin_data::translation;
use pumpkin_protocol::{
ConnectionState, KnownPack, Label, Link, LinkType,
java::client::{
config::{
CConfigAddResourcePack, CConfigServerLinks, CFeatureFlags, CFinishConfig, CKnownPacks,
CRegistryData, CUpdateTags,
},
login::{CLoginSuccess, CSetCompression},
},
java::server::config::SKnownPacks,
java::server::login::{
SEncryptionResponse, SLoginCookieResponse, SLoginPluginResponse, SLoginStart,
},
};
use pumpkin_util::{text::TextComponent, version::JavaMinecraftVersion};
use std::sync::Arc;
use tracing::debug;
use uuid::Uuid;
use crate::{
net::{
GameProfile,
authentication::{self, AuthError},
is_valid_player_name,
java::pending::PendingConnection,
offline_uuid,
proxy::{bungeecord, velocity},
},
server::Server,
};
pub mod cookie_response;
pub mod encryption_response;
pub mod known_packs;
pub mod login_acknowledged;
pub mod login_start;
pub mod plugin_response;

View File

@ -0,0 +1,27 @@
#[allow(clippy::wildcard_imports)]
use super::*;
impl PendingConnection {
pub async fn handle_plugin_response(
&mut self,
server: &Server,
plugin_response: SLoginPluginResponse,
) {
debug!("Handling plugin");
let velocity_config = &server.advanced_config.networking.proxy.velocity;
if velocity_config.enabled {
match velocity::receive_velocity_plugin_response(
self.address.port(),
velocity_config,
plugin_response,
) {
Ok((profile, new_address)) => {
self.finish_login(&profile).await;
self.gameprofile = Some(profile);
self.address = new_address;
}
Err(error) => self.kick(TextComponent::text(error.to_string())).await,
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,58 @@
#[allow(clippy::wildcard_imports)]
use super::*;
impl JavaClient {
pub async fn handle_attack(&self, player: &Arc<Player>, attack: SAttack, server: &Arc<Server>) {
if !player.has_client_loaded() {
return;
}
player.update_last_action_time();
let entity_id = attack.entity_id;
let player_entity = &player.get_entity();
let world = player_entity.world.load_full();
let config = &server.advanced_config.pvp;
if !config.enabled {
return;
}
if entity_id.0 == player.entity_id() {
self.kick(TextComponent::translate_cross(
translation::java::MULTIPLAYER_DISCONNECT_INVALID_ENTITY_ATTACKED,
translation::java::MULTIPLAYER_DISCONNECT_INVALID_ENTITY_ATTACKED,
[],
))
.await;
return;
}
let player_target = world.get_player_by_id(entity_id.0);
let target: Option<Arc<dyn EntityBase>> = player_target
.as_ref()
.map(|p| Arc::clone(p) as Arc<dyn EntityBase>)
.or_else(|| world.get_entity_by_id(entity_id.0));
let Some(target) = target else {
self.kick(TextComponent::translate_cross(
translation::java::MULTIPLAYER_DISCONNECT_INVALID_ENTITY_ATTACKED,
translation::java::MULTIPLAYER_DISCONNECT_INVALID_ENTITY_ATTACKED,
[],
))
.await;
return;
};
if let Some(player_victim) = &player_target {
if player_victim.living_entity.health.load() <= 0.0 {
return;
}
if config.protect_creative && player_victim.gamemode.load() == GameMode::Creative {
world.play_sound(
Sound::EntityPlayerAttackNodamage,
SoundCategory::Players,
&player_victim.position(),
);
return;
}
}
player.attack(target).await;
}
}

View File

@ -0,0 +1,27 @@
#[allow(clippy::wildcard_imports)]
use super::*;
impl JavaClient {
pub async fn handle_bundle_item_selected(
&self,
player: &Arc<Player>,
packet: SBundleItemSelected,
) {
if !player.has_client_loaded() {
return;
}
player.update_last_action_time();
let selected_item_index = packet.selected_item_index.0;
if selected_item_index < 0 && selected_item_index != -1 {
self.kick(TextComponent::text("Invalid selected item index"))
.await;
return;
}
debug!(
"Bundle item selected: Slot ID {}, Selected Item Index {}",
packet.slot_id.0, selected_item_index
);
}
}

View File

@ -0,0 +1,26 @@
#[allow(clippy::wildcard_imports)]
use super::*;
impl JavaClient {
pub async fn handle_change_game_mode(
&self,
player: &Arc<Player>,
change_game_mode: SChangeGameMode,
) {
if player.permission_lvl.load() >= PermissionLvl::Two {
player.set_gamemode(change_game_mode.game_mode).await;
let gamemode_string = format!("gameMode.{}", change_game_mode.game_mode.name());
player
.send_system_message(&TextComponent::translate_cross(
translation::java::COMMANDS_GAMEMODE_SUCCESS_SELF,
translation::bedrock::COMMANDS_GAMEMODE_SUCCESS_SELF,
[TextComponent::translate_cross(
gamemode_string.clone(),
gamemode_string,
[],
)],
))
.await;
}
}
}

View File

@ -0,0 +1,45 @@
#[allow(clippy::wildcard_imports)]
use super::*;
impl JavaClient {
pub async fn handle_chat_command(
&self,
player: &Arc<Player>,
server: &Arc<Server>,
command: &SChatCommand<'_>,
) {
player.update_last_action_time();
let player_clone = player.clone();
let server_clone = server.clone();
send_cancellable! {{
server;
PlayerCommandSendEvent {
player: player.clone(),
command: command.command.to_string(),
cancelled: false
};
'after: {
let command = event.command;
let command_clone = command.clone();
// Some commands can take a long time to execute. If they do, they block packet processing for the player.
// That's why we will spawn a task instead.
server.spawn_task(async move {
let dispatcher = server_clone.command_dispatcher.read().await;
dispatcher.handle_command(
&player_clone.get_command_source(&server_clone).await,
&command_clone
).await;
});
if server.advanced_config.commands.log_console {
info!(
"Player ({}): executed command /{}",
player.gameprofile.name,
command
);
}
}
}}
}
}

View File

@ -0,0 +1,220 @@
#[allow(clippy::wildcard_imports)]
use super::*;
impl JavaClient {
pub async fn handle_chat_message(
&self,
server: &Arc<Server>,
player: &Arc<Player>,
chat_message: SChatMessage<'_>,
) {
player.update_last_action_time();
let gameprofile = &player.gameprofile;
if let Err(err) = self
.validate_chat_message(server, player, &chat_message)
.await
{
log_at_level!(
err.severity(),
"{} (uuid {}) {}",
gameprofile.name,
gameprofile.id,
err
);
if err.is_kick()
&& let Some(reason) = err.client_kick_reason()
{
self.kick(TextComponent::text(reason)).await;
}
return;
}
send_cancellable! {{
server;
PlayerChatEvent::new(player.clone(), chat_message.message.to_string(), vec![]);
'after: {
info!("<chat> {}: {}", gameprofile.name, event.message);
let config = &server.advanced_config;
let message = match seasonal_events::modify_chat_message(&event.message, config) {
Some(m) => m,
None => event.message.clone(),
};
let decorated_message = TextComponent::chat_decorated(
&config.chat.format,
&gameprofile.name,
&message,
);
let entity = &player.get_entity();
let world = entity.world.load_full();
if server.basic_config.allow_chat_reports {
world.broadcast_secure_player_chat(player, &chat_message, &decorated_message).await;
} else {
let je_packet = CSystemChatMessage::new(
&decorated_message,
false,
);
let be_packet = SText::new(
message, player.gameprofile.name.clone()
);
world.broadcast_editioned(&je_packet, &be_packet).await;
}
}
}}
}
/// Runs all vanilla checks for a valid chat message
pub async fn validate_chat_message(
&self,
server: &Server,
player: &Arc<Player>,
chat_message: &SChatMessage<'_>,
) -> Result<(), ChatError> {
// Check for oversized messages
// If we're able to find the 257th UTF-16 character, the message is too big.
if chat_message.message.encode_utf16().nth(256).is_some() {
return Err(ChatError::OversizedMessage);
}
// Check for illegal characters
if chat_message
.message
.chars()
.any(|c| c == '§' || c < ' ' || c == '\x7F')
{
return Err(ChatError::IllegalCharacters);
}
// These checks are only run in secure chat mode
if server.basic_config.allow_chat_reports {
// Check for unsigned chat
if let Some(signature) = &chat_message.signature {
if signature.len() != 256 {
return Err(ChatError::UnsignedChat); // Signature is the wrong length
}
} else {
return Err(ChatError::UnsignedChat); // There is no signature
}
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as i64;
// Verify message timestamp
if chat_message.timestamp > now || chat_message.timestamp < (now - CHAT_MESSAGE_MAX_AGE)
{
return Err(ChatError::OutOfOrderChat);
}
// Verify session expiry
if player.chat_session.lock().await.expires_at < now {
return Err(ChatError::ExpiredPublicKey);
}
// Validate previous signature checksum (new in 1.21.5)
// The client can bypass this check by sending 0
if chat_message.checksum != 0 {
let checksum =
polynomial_rolling_hash(player.signature_cache.lock().await.last_seen.as_ref());
if checksum != chat_message.checksum {
return Err(ChatError::ChatValidationFailed);
}
}
}
Ok(())
}
pub async fn handle_chat_session_update(
&self,
player: &Arc<Player>,
server: &Server,
session: SPlayerSession,
) {
// Keep the chat session default if we don't want reports
if !server.basic_config.allow_chat_reports {
return;
}
if let Err(err) = self.validate_chat_session(player, server, &session) {
log_at_level!(
err.severity(),
"{} (uuid {}) {}",
player.gameprofile.name,
player.gameprofile.id,
err
);
if err.is_kick()
&& let Some(reason) = err.client_kick_reason()
{
self.kick(TextComponent::text(reason)).await;
}
return;
}
// Update the chat session fields
*player.chat_session.lock().await = ChatSession::new(
session.session_id,
session.expires_at,
session.public_key.clone(),
session.key_signature.clone(),
);
server.broadcast_packet_all(&CPlayerInfoUpdate::new(
0x02,
&[pumpkin_protocol::java::client::play::Player {
uuid: player.gameprofile.id,
actions: &[PlayerAction::InitializeChat(Some(InitChat {
session_id: session.session_id,
expires_at: session.expires_at,
public_key: session.public_key.clone(),
signature: session.key_signature.clone(),
}))],
}],
));
}
/// Runs vanilla checks for a valid player session
pub fn validate_chat_session(
&self,
player: &Player,
server: &Server,
session: &SPlayerSession,
) -> Result<(), ChatError> {
// Verify session expiry
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as i64;
if session.expires_at < now {
return Err(ChatError::InvalidPublicKey);
}
let key_signature = RsaPkcs1v15Signature::try_from(session.key_signature.as_ref())
.map_err(|_| ChatError::InvalidPublicKey)?;
let mut signable = Vec::new();
signable.extend_from_slice(player.gameprofile.id.as_bytes());
signable.extend_from_slice(&session.expires_at.to_be_bytes());
signable.extend_from_slice(&session.public_key);
let public_keys_guard = server.mojang_public_keys.load();
// Verify signature with RSA-SHA1
let is_valid = public_keys_guard.iter().any(|key| {
let verifying_key = VerifyingKey::<Sha1>::new(key.clone());
verifying_key.verify(&signable, &key_signature).is_ok()
});
// Verify that the signable is valid for any one of Mojang's public keys
if !is_valid {
return Err(ChatError::InvalidPublicKey);
}
Ok(())
}
}

View File

@ -0,0 +1,16 @@
#[allow(clippy::wildcard_imports)]
use super::*;
impl JavaClient {
pub async fn handle_chunk_batch(&self, player: &Player, packet: SChunkBatch) {
player
.chunk_manager
.lock()
.await
.handle_acknowledge(packet.chunks_per_tick);
trace!(
"Client requested {} chunks per tick",
packet.chunks_per_tick
);
}
}

View File

@ -0,0 +1,38 @@
#[allow(clippy::wildcard_imports)]
use super::*;
impl JavaClient {
pub async fn handle_client_status(&self, player: &Arc<Player>, client_status: SClientCommand) {
player.update_last_action_time();
match client_status.action_id.0 {
0 => {
// Perform respawn
if player.living_entity.health.load() > 0.0 {
return;
}
player.world().clone().respawn_player(player, false).await;
{
let screen_handler = player.current_screen_handler.lock().await;
let mut screen_handler = screen_handler.lock().await;
screen_handler.sync_state().await;
};
// Restore abilities based on gamemode after respawn
{
let mut abilities = player.abilities.lock().await;
abilities.set_for_gamemode(player.gamemode.load());
};
player.send_abilities_update().await;
}
1 => {
// Request stats
player.send_stats().await;
}
_ => {
self.kick(TextComponent::text("Invalid client status"))
.await;
}
}
}
}

View File

@ -0,0 +1,89 @@
#[allow(clippy::wildcard_imports)]
use super::*;
impl JavaClient {
pub async fn handle_client_information(
&self,
server: &Arc<Server>,
player: &Arc<Player>,
client_information: SClientInformationPlay<'_>,
) {
if let (Ok(main_hand), Ok(chat_mode)) = (
Hand::try_from(client_information.main_hand.0),
ChatMode::try_from(client_information.chat_mode.0),
) {
if client_information.view_distance <= 0 {
self.kick(TextComponent::text(
"Cannot have zero or negative view distance!",
))
.await;
return;
}
let (update_settings, update_watched, main_hand_changed) = {
// 1. Load current snapshot
let current_config = player.config.load();
// 2. Calculate if settings changed before we overwrite
let main_hand_changed = current_config.main_hand != main_hand;
let update_settings =
main_hand_changed || current_config.skin_parts != client_information.skin_parts;
let old_view_distance = current_config.view_distance;
let new_view_distance_raw = client_information.view_distance as u8;
let update_watched = if old_view_distance.get() == new_view_distance_raw {
false
} else {
debug!(
"Player {} ({}) updated their render distance: {} -> {}.",
player.gameprofile.name, self.id, old_view_distance, new_view_distance_raw
);
true
};
// 3. Construct the new config
// If view_distance is 0, we exit early (safe guard)
let Some(new_view_distance) = NonZeroU8::new(new_view_distance_raw) else {
return;
};
let new_config = PlayerConfig {
locale: client_information.locale.to_string(),
view_distance: new_view_distance,
chat_mode,
chat_colors: client_information.chat_colors,
skin_parts: client_information.skin_parts,
main_hand,
text_filtering: client_information.text_filtering,
server_listing: client_information.server_listing,
};
// 4. Atomically swap the new config into the player
player.config.store(std::sync::Arc::new(new_config));
(update_settings, update_watched, main_hand_changed)
};
if update_watched {
chunker::update_position(player).await;
}
if main_hand_changed {
let mut event = PlayerChangedMainHandEvent::new(player.clone(), main_hand);
server.plugin_manager.fire(server, &mut event).await;
}
if update_settings {
debug!(
"Player {} ({}) updated their skin.",
player.gameprofile.name, self.id,
);
player.send_client_information();
}
} else {
self.kick(TextComponent::text("Invalid hand or chat type"))
.await;
}
}
}

View File

@ -0,0 +1,13 @@
#[allow(clippy::wildcard_imports)]
use super::*;
impl JavaClient {
pub async fn handle_close_container(
&self,
player: &Arc<Player>,
_server: &Server,
_packet: SCloseContainer,
) {
player.on_handled_screen_closed().await;
}
}

View File

@ -0,0 +1,36 @@
#[allow(clippy::wildcard_imports)]
use super::*;
impl JavaClient {
pub async fn handle_command_suggestion(
&self,
player: &Arc<Player>,
packet: SCommandSuggestion<'_>,
server: &Arc<Server>,
) {
let Some(cmd) = &packet.command.get(1..) else {
return;
};
let Some((last_word_start, _)) = cmd.char_indices().rfind(|(_, c)| c.is_whitespace())
else {
return;
};
let suggestions = server
.command_dispatcher
.read()
.await
.suggest(cmd, &player.get_command_source(server).await)
.await;
let response = CCommandSuggestions::new(
packet.id,
((last_word_start + 2) as i32).into(),
((cmd.len() - last_word_start - 1) as i32).into(),
suggestions.into(),
);
self.enqueue_packet(&response).await;
}
}

View File

@ -0,0 +1,31 @@
#[allow(clippy::wildcard_imports)]
use super::*;
impl JavaClient {
pub async fn handle_confirm_teleport(
&self,
player: &Player,
confirm_teleport: SConfirmTeleport,
) {
let mut awaiting_teleport = player.awaiting_teleport.lock().await;
if let Some((id, position)) = awaiting_teleport.as_ref() {
if id == &confirm_teleport.teleport_id {
// We should set the position now to what we requested in the teleport packet.
// This may fix issues when the client sends the position while being teleported.
player.get_entity().set_pos(*position);
*awaiting_teleport = None;
drop(awaiting_teleport);
} else {
drop(awaiting_teleport);
self.kick(TextComponent::text("Wrong teleport id")).await;
}
} else {
drop(awaiting_teleport);
self.kick(TextComponent::text(
"Send Teleport confirm, but we did not teleport",
))
.await;
}
}
}

View File

@ -0,0 +1,13 @@
#[allow(clippy::wildcard_imports)]
use super::*;
impl JavaClient {
pub fn handle_cookie_response(&self, packet: &SPCookieResponse<'_>) {
// TODO: allow plugins to access this
debug!(
"Received cookie_response[play]: key: \"{}\", payload_length: \"{:?}\"",
packet.key,
packet.payload.as_ref().map(|p| p.len())
);
}
}

View File

@ -0,0 +1,16 @@
#[allow(clippy::wildcard_imports)]
use super::*;
impl JavaClient {
pub fn handle_debug_sample_subscription(
&self,
player: &Arc<Player>,
packet: &pumpkin_protocol::java::server::play::SDebugSampleSubscription,
) {
if player.permission_lvl.load() >= PermissionLvl::Two && packet.sample_type.0 == 0 {
player
.subscribed_debug_sample
.store(true, Ordering::Relaxed);
}
}
}

View File

@ -0,0 +1,16 @@
#[allow(clippy::wildcard_imports)]
use super::*;
impl JavaClient {
pub fn handle_debug_subscription_request(
&self,
player: &Arc<Player>,
packet: &pumpkin_protocol::java::server::play::SDebugSubscriptionRequest,
) {
if player.permission_lvl.load() >= PermissionLvl::Two && packet.sample_type.0 == 0 {
player
.subscribed_debug_sample
.store(true, Ordering::Relaxed);
}
}
}

View File

@ -0,0 +1,36 @@
#[allow(clippy::wildcard_imports)]
use super::*;
impl JavaClient {
pub async fn handle_edit_book(&self, player: &Player, packet: SEditBook<'_>) {
let held_stack = player.inventory().held_item().await;
if held_stack.item.id != Item::WRITABLE_BOOK.id {
return;
}
let pages: Vec<String> = packet.pages.iter().map(|p| (*p).to_string()).collect();
if let Some(title) = packet.title {
let mut written_book = ItemStack::new(1, &Item::WRITTEN_BOOK);
let content = WrittenBookContentImpl {
title: title.to_string(),
author: player.gameprofile.name.clone(),
pages,
};
written_book
.patch
.push((DataComponent::WrittenBookContent, Some(content.to_dyn())));
player.inventory().set_held_item(written_book).await;
} else {
let mut writable_book = held_stack;
let content = WritableBookContentImpl { pages };
writable_book
.patch
.retain(|(component, _)| *component != DataComponent::WritableBookContent);
writable_book
.patch
.push((DataComponent::WritableBookContent, Some(content.to_dyn())));
player.inventory().set_held_item(writable_book).await;
}
}
}

View File

@ -0,0 +1,129 @@
#[allow(clippy::wildcard_imports)]
use super::*;
impl JavaClient {
#[expect(clippy::too_many_lines)]
pub async fn handle_interact(
&self,
player: &Arc<Player>,
interact: SInteract,
server: &Arc<Server>,
) {
if !player.has_client_loaded() {
return;
}
player.update_last_action_time();
let entity_id = interact.entity_id;
let sneaking = interact.sneaking;
let player_entity = &player.get_entity();
if player_entity.is_sneaking() != sneaking {
player_entity.set_sneaking(sneaking).await;
}
let Ok(action) = ActionType::try_from(interact.r#type.0) else {
self.kick(TextComponent::text("Invalid action type")).await;
return;
};
// Resolve the target entity for the event
let world = player_entity.world.load_full();
let player_target = world.get_player_by_id(entity_id.0);
let target: Option<Arc<dyn EntityBase>> = player_target
.as_ref()
.map(|p| Arc::clone(p) as Arc<dyn EntityBase>)
.or_else(|| world.get_entity_by_id(entity_id.0));
if let Some(target) = target {
if player.gamemode.load() == GameMode::Spectator {
player.camera_target_id.store(Some(entity_id.0));
player
.client
.send_packet_now(&CSetCamera::new(entity_id))
.await;
return;
}
send_cancellable! {{
server;
PlayerInteractEntityEvent::new(
player,
Arc::clone(&target),
action.clone(),
interact.target_position,
sneaking,
);
'after: {
match event.action {
ActionType::Attack => {
let config = &server.advanced_config.pvp;
if !config.enabled {
return;
}
if entity_id.0 == player.entity_id() {
self.kick(TextComponent::translate_cross(translation::java::MULTIPLAYER_DISCONNECT_INVALID_ENTITY_ATTACKED, translation::java::MULTIPLAYER_DISCONNECT_INVALID_ENTITY_ATTACKED, [],))
.await;
return;
}
if let Some(player_victim) = &player_target {
if player_victim.living_entity.health.load() <= 0.0 {
return;
}
if config.protect_creative
&& player_victim.gamemode.load() == GameMode::Creative
{
world
.play_sound(
Sound::EntityPlayerAttackNodamage,
SoundCategory::Players,
&player_victim.position(),
)
;
return;
}
}
player.attack(event.target).await;
}
ActionType::Interact | ActionType::InteractAt => {
let mut stack = player.inventory().held_item().await;
let target_entity = event.target.get_entity();
if target_entity.entity_type.resource_name == "zombie_villager"
&& stack.item.registry_key == "golden_apple"
{
player.trigger_advancement(crate::entity::player::advancement::trigger::AdvancementTrigger::CuredZombieVillager).await;
}
let interacted = event.target.interact(player, &mut stack).await;
if !interacted {
server
.item_registry
.use_on_entity(&mut stack, player, event.target)
.await;
}
player.inventory().set_held_item(stack).await;
}
}
}
}}
} else {
// Entity not found
send_cancellable! {{
server;
PlayerInteractUnknownEntityEvent::new(player, entity_id.0, action);
'after: {
if event.action == ActionType::Attack {
error!(
"Player id {} interacted with entity id {}, which was not found.",
player.entity_id(),
event.entity_id
);
self.kick(TextComponent::translate_cross(translation::java::MULTIPLAYER_DISCONNECT_INVALID_ENTITY_ATTACKED, translation::java::MULTIPLAYER_DISCONNECT_INVALID_ENTITY_ATTACKED, [],))
.await;
}
}
}}
}
}
}

View File

@ -0,0 +1,21 @@
#[allow(clippy::wildcard_imports)]
use super::*;
impl JavaClient {
pub async fn handle_jigsaw_generate(&self, player: &Arc<Player>, generate: SJigsawGenerate) {
if !player.is_creative() {
return;
}
if player.permission_lvl.load() < PermissionLvl::Two {
return;
}
let pos = generate.pos;
if let Some(block_entity) = player.world().get_block_entity(&pos)
&& let Some(jigsaw_block) = block_entity.as_any().downcast_ref::<JigsawBlockEntity>()
{
jigsaw_block
.generate(&player.world(), generate.levels.0, generate.keep_jigsaws)
.await;
}
}
}

View File

@ -0,0 +1,24 @@
#[allow(clippy::wildcard_imports)]
use super::*;
impl JavaClient {
pub async fn handle_keep_alive(&self, player: &Player, keep_alive: SKeepAlive) {
if self.wait_for_keep_alive.load(Ordering::Relaxed)
&& keep_alive.keep_alive_id == self.keep_alive_id.load()
{
let ping = self.last_keep_alive_time.load().elapsed();
// Vanilla logic
player.ping.store(
(player.ping.load(Ordering::Relaxed) * 3 + ping.as_millis() as u32) / 4,
Ordering::Relaxed,
);
self.wait_for_keep_alive.store(false, Ordering::Relaxed);
} else {
self.kick(pumpkin_macros::translate_cross!(
translation::java::DISCONNECT_TIMEOUT,
translation::bedrock::DISCONNECT_TIMEOUT
))
.await;
}
}
}

View File

@ -0,0 +1,265 @@
use pumpkin_protocol::bedrock::server::text::SText;
use pumpkin_util::{Hand, PermissionLvl};
use rsa::pkcs1v15::{Signature as RsaPkcs1v15Signature, VerifyingKey};
use rsa::signature::Verifier;
use sha1::Sha1;
use std::num::NonZeroU8;
use std::sync::Arc;
use std::sync::atomic::{AtomicU32, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
use thiserror::Error;
use tracing::{Level, debug, error, info, trace, warn};
use crate::block::BlockHitResult;
use crate::block::registry::BlockActionResult;
use crate::block::{self};
use crate::entity::EntityBase;
use crate::entity::equipment_break_status;
use crate::entity::player::statistics::{CustomStatistic, StatisticCategory};
use crate::entity::player::{ChatMode, ChatSession, MINE_BLOCK_EXHAUSTION, Player};
use crate::error::PumpkinError;
use crate::log_at_level;
use crate::net::PlayerConfig;
use crate::net::java::JavaClient;
use crate::plugin::player::changed_main_hand::PlayerChangedMainHandEvent;
use crate::plugin::player::fish::{PlayerFishEvent, PlayerFishState};
use crate::plugin::player::item_held::PlayerItemHeldEvent;
use crate::plugin::player::player_chat::PlayerChatEvent;
use crate::plugin::player::player_command_send::PlayerCommandSendEvent;
use crate::plugin::player::player_interact_entity_event::PlayerInteractEntityEvent;
use crate::plugin::player::player_interact_event::{InteractAction, PlayerInteractEvent};
use crate::plugin::player::player_interact_unknown_entity_event::PlayerInteractUnknownEntityEvent;
use crate::plugin::player::player_move::PlayerMoveEvent;
use crate::plugin::player::player_toggle_flight_event::PlayerToggleFlightEvent;
use crate::plugin::player::player_toggle_sneak_event::PlayerToggleSneakEvent;
use crate::block::entities::command_block::CommandBlockEntity;
use crate::block::entities::jigsaw_block::JigsawBlockEntity;
use crate::block::entities::sign::SignBlockEntity;
use crate::plugin::player::player_toggle_sprint_event::PlayerToggleSprintEvent;
use crate::server::{Server, seasonal_events};
use crate::world::{BlockBreakingProgress, World, chunker};
use pumpkin_data::block_properties::{BlockProperties, CommandBlockLikeProperties};
use pumpkin_data::data_component::DataComponent;
use pumpkin_data::data_component_impl::{
BlocksAttacksImpl, ConsumableImpl, DataComponentImpl, EquipmentSlot, EquippableImpl, FoodImpl,
WritableBookContentImpl, WrittenBookContentImpl,
};
use pumpkin_data::item::Item;
use pumpkin_data::item_stack::ItemStack;
use pumpkin_data::sound::{Sound, SoundCategory};
use pumpkin_data::{Advancement, Block, BlockDirection, translation};
use pumpkin_inventory::InventoryError;
use pumpkin_inventory::merchant::merchant_screen_handler::MerchantScreenHandler;
use pumpkin_inventory::player::player_inventory::PlayerInventory;
use pumpkin_inventory::screen_handler::{InventoryPlayer, ScreenHandler};
use pumpkin_protocol::bedrock::client::CMovePlayer;
use pumpkin_protocol::codec::var_int::VarInt;
use pumpkin_protocol::codec::var_ulong::VarULong;
use pumpkin_protocol::java::client::play::{
CBlockUpdate, CCommandSuggestions, CEntityPositionSync, CHeadRot, COpenSignEditor,
CPingResponse, CPlayerInfoUpdate, CPlayerPosition, CSetCamera, CSetSelectedSlot,
CSystemChatMessage, CUpdateEntityPos, CUpdateEntityPosRot, CUpdateEntityRot, InitChat,
PlayerAction,
};
use pumpkin_protocol::java::server::play::{
Action, ActionType, CommandBlockMode, FLAG_ON_GROUND, SAttack, SBundleItemSelected,
SChangeGameMode, SChatCommand, SChatMessage, SChunkBatch, SClientCommand,
SClientInformationPlay, SCloseContainer, SCommandSuggestion, SConfirmTeleport,
SCookieResponse as SPCookieResponse, SEditBook, SInteract, SJigsawGenerate, SKeepAlive,
SMoveVehicle, SPaddleBoat, SPickItemFromBlock, SPickItemFromEntity, SPlaceRecipe,
SPlayPingRequest, SPlayerAbilities, SPlayerAction, SPlayerCommand, SPlayerInput,
SPlayerPosition, SPlayerPositionRotation, SPlayerRotation, SPlayerSession,
SRecipeBookChangeSettings, SRecipeBookSeenRecipe, SSeenAdvancement, SSelectTrade,
SSetCommandBlock, SSetCreativeSlot, SSetHeldItem, SSetJigsawBlock, SSetPlayerGround,
SSetTestBlock, SSwingArm, STeleportToEntity, STestInstanceBlockAction, SUpdateSign, SUseItem,
SUseItemOn, Status,
};
use pumpkin_util::math::vector3::Vector3;
use pumpkin_util::math::{polynomial_rolling_hash, position::BlockPos, wrap_degrees};
use pumpkin_util::{GameMode, text::TextComponent};
use pumpkin_world::generation::structure::structures::jigsaw::JigsawJointType;
use pumpkin_world::world::BlockFlags;
use tokio::sync::Mutex;
/// In secure chat mode, Player will be kicked if they send a chat message with a timestamp that is older than this (in ms)
/// Vanilla: 2 minutes
const CHAT_MESSAGE_MAX_AGE: i64 = 1000 * 60 * 2;
#[derive(Debug, Error)]
pub enum BlockPlacingError {
BlockOutOfReach,
InvalidHand,
InvalidBlockFace,
BlockOutOfWorld,
InvalidGamemode,
}
impl std::fmt::Display for BlockPlacingError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{self:?}")
}
}
impl PumpkinError for BlockPlacingError {
fn is_kick(&self) -> bool {
match self {
Self::BlockOutOfReach | Self::BlockOutOfWorld | Self::InvalidGamemode => false,
Self::InvalidBlockFace | Self::InvalidHand => true,
}
}
fn severity(&self) -> Level {
match self {
Self::BlockOutOfWorld | Self::InvalidGamemode => Level::TRACE,
Self::BlockOutOfReach | Self::InvalidBlockFace | Self::InvalidHand => Level::WARN,
}
}
fn client_kick_reason(&self) -> Option<String> {
match self {
Self::BlockOutOfReach | Self::BlockOutOfWorld | Self::InvalidGamemode => None,
Self::InvalidBlockFace => Some("Invalid block face".into()),
Self::InvalidHand => Some("Invalid hand".into()),
}
}
}
#[derive(Debug, Error)]
pub enum ChatError {
#[error("sent an oversized message")]
OversizedMessage,
#[error("sent a message with illegal characters")]
IllegalCharacters,
#[error("sent a chat with invalid/no signature")]
UnsignedChat,
#[error("has too many unacknowledged chats queued")]
TooManyPendingChats,
#[error("sent a chat that couldn't be validated")]
ChatValidationFailed,
#[error("sent a chat with an out of order timestamp")]
OutOfOrderChat,
#[error("has an expired public key")]
ExpiredPublicKey,
#[error("attempted to initialize a session with an invalid public key")]
InvalidPublicKey,
}
impl PumpkinError for ChatError {
fn is_kick(&self) -> bool {
true
}
fn severity(&self) -> Level {
Level::WARN
}
fn client_kick_reason(&self) -> Option<String> {
match self {
Self::OversizedMessage => Some("Chat message too long".into()),
Self::IllegalCharacters => Some(
TextComponent::translate_cross(
translation::java::MULTIPLAYER_DISCONNECT_ILLEGAL_CHARACTERS,
translation::java::MULTIPLAYER_DISCONNECT_ILLEGAL_CHARACTERS,
[],
)
.get_text(),
),
Self::UnsignedChat => Some(
TextComponent::translate_cross(
translation::java::MULTIPLAYER_DISCONNECT_UNSIGNED_CHAT,
translation::java::MULTIPLAYER_DISCONNECT_UNSIGNED_CHAT,
[],
)
.get_text(),
),
Self::TooManyPendingChats => Some(
TextComponent::translate_cross(
translation::java::MULTIPLAYER_DISCONNECT_TOO_MANY_PENDING_CHATS,
translation::java::MULTIPLAYER_DISCONNECT_TOO_MANY_PENDING_CHATS,
[],
)
.get_text(),
),
Self::ChatValidationFailed => Some(
TextComponent::translate_cross(
translation::java::MULTIPLAYER_DISCONNECT_CHAT_VALIDATION_FAILED,
translation::java::MULTIPLAYER_DISCONNECT_CHAT_VALIDATION_FAILED,
[],
)
.get_text(),
),
Self::OutOfOrderChat => Some(
TextComponent::translate_cross(
translation::java::MULTIPLAYER_DISCONNECT_OUT_OF_ORDER_CHAT,
translation::java::MULTIPLAYER_DISCONNECT_OUT_OF_ORDER_CHAT,
[],
)
.get_text(),
),
Self::ExpiredPublicKey => Some(
TextComponent::translate_cross(
translation::java::MULTIPLAYER_DISCONNECT_EXPIRED_PUBLIC_KEY,
translation::java::MULTIPLAYER_DISCONNECT_EXPIRED_PUBLIC_KEY,
[],
)
.get_text(),
),
Self::InvalidPublicKey => Some(
TextComponent::translate_cross(
translation::java::MULTIPLAYER_DISCONNECT_INVALID_PUBLIC_KEY_SIGNATURE,
translation::java::MULTIPLAYER_DISCONNECT_INVALID_PUBLIC_KEY_SIGNATURE,
[],
)
.get_text(),
),
}
}
}
pub mod attack;
pub mod bundle_item_selected;
pub mod change_game_mode;
pub mod chat_command;
pub mod chat_message;
pub mod chunk_batch;
pub mod client_command;
pub mod client_information;
pub mod close_container;
pub mod command_suggestion;
pub mod confirm_teleport;
pub mod cookie_response;
pub mod debug_sample_subscription;
pub mod debug_subscription_request;
pub mod edit_book;
pub mod interact;
pub mod jigsaw_generate;
pub mod keep_alive;
pub mod move_vehicle;
pub mod paddle_boat;
pub mod pick_item;
pub mod ping_request;
pub mod place_recipe;
pub mod player_abilities;
pub mod player_action;
pub mod player_command;
pub mod player_ground;
pub mod player_input;
pub mod player_loaded;
pub mod player_position;
pub mod player_rotation;
pub mod recipe_book_change_settings;
pub mod recipe_book_seen_recipe;
pub mod seen_advancement;
pub mod select_trade;
pub mod set_command_block;
pub mod set_creative_slot;
pub mod set_held_item;
pub mod set_jigsaw_block;
pub mod set_test_block;
pub mod swing_arm;
pub mod teleport_to_entity;
pub mod test_instance_block_action;
pub mod update_sign;
pub mod use_item;
pub mod use_item_on;

View File

@ -0,0 +1,18 @@
#[allow(clippy::wildcard_imports)]
use super::*;
impl JavaClient {
pub async fn handle_move_vehicle(&self, player: &Arc<Player>, packet: SMoveVehicle) {
let entity = player.get_entity();
let pos = Vector3::new(packet.x, packet.y, packet.z);
let vehicle = entity.vehicle.lock().await;
if let Some(vehicle) = vehicle.as_ref() {
let vehicle_entity = vehicle.get_entity();
vehicle_entity.set_pos(pos);
vehicle_entity.set_rotation(packet.yaw, packet.pitch);
}
drop(vehicle);
entity.set_pos(pos);
chunker::update_position(player).await;
}
}

View File

@ -0,0 +1,13 @@
#[allow(clippy::wildcard_imports)]
use super::*;
impl JavaClient {
pub async fn handle_paddle_boat(&self, player: &Arc<Player>, packet: SPaddleBoat) {
let vehicle = player.get_entity().vehicle.lock().await.clone();
if let Some(vehicle) = vehicle {
vehicle
.set_paddle_state(packet.left_paddle, packet.right_paddle)
.await;
}
}
}

View File

@ -0,0 +1,120 @@
#[allow(clippy::wildcard_imports)]
use super::*;
impl JavaClient {
pub async fn handle_pick_item_from_block(
&self,
player: &Arc<Player>,
pick_item: SPickItemFromBlock,
) {
if !player.can_interact_with_block_at(&pick_item.pos, 1.0) {
return;
}
let world = player.world();
let block = world.get_block(&pick_item.pos);
if block.item_id == 0 {
// Invalid block id (blocks such as tall seagrass)
return;
}
let Some(item) = Item::from_id(block.item_id) else {
return;
};
let stack = ItemStack::new(1, item);
let slot_with_stack = player.inventory().get_slot_with_stack(&stack).await;
if slot_with_stack != -1 {
if PlayerInventory::is_valid_hotbar_index(slot_with_stack as usize) {
player.inventory.set_selected_slot(slot_with_stack as u8);
} else {
player
.inventory
.swap_slot_with_hotbar(slot_with_stack as usize)
.await;
}
} else if player.gamemode.load() == GameMode::Creative {
player.inventory.swap_stack_with_hotbar(stack).await;
}
player
.client
.enqueue_packet(&CSetSelectedSlot::new(
player.inventory.get_selected_slot() as i8
))
.await;
player
.player_screen_handler
.lock()
.await
.send_content_updates()
.await;
}
pub async fn handle_pick_item_from_entity(
&self,
player: &Arc<Player>,
pick_item: SPickItemFromEntity,
) {
use pumpkin_data::entity::{entity_from_egg, spawn_egg_ids};
let world = player.world();
let Some(target) = world.get_entity_by_id(pick_item.id.0) else {
return;
};
let p_eye = player.get_entity().get_eye_pos();
let t_eye = target.get_eye_pos();
let dx = p_eye.x - t_eye.x;
let dy = p_eye.y - t_eye.y;
let dz = p_eye.z - t_eye.z;
if dx * dx + dy * dy + dz * dz > 64.0 {
return;
}
let target_type_id = target.get_entity().entity_type.id;
let mut found_egg: Option<u16> = None;
for &egg_id in &spawn_egg_ids() {
if let Some(et) = entity_from_egg(egg_id)
&& et.id == target_type_id
{
found_egg = Some(egg_id);
break;
}
}
if let Some(item) = found_egg.and_then(Item::from_id) {
let stack = ItemStack::new(1, item);
let slot_with_stack = player.inventory().get_slot_with_stack(&stack).await;
if slot_with_stack != -1 {
if PlayerInventory::is_valid_hotbar_index(slot_with_stack as usize) {
player.inventory.set_selected_slot(slot_with_stack as u8);
} else {
player
.inventory
.swap_slot_with_hotbar(slot_with_stack as usize)
.await;
}
} else if player.gamemode.load() == GameMode::Creative {
player.inventory.swap_stack_with_hotbar(stack).await;
}
player
.client
.enqueue_packet(&CSetSelectedSlot::new(
player.inventory.get_selected_slot() as i8
))
.await;
player
.player_screen_handler
.lock()
.await
.send_content_updates()
.await;
}
}
}

View File

@ -0,0 +1,9 @@
#[allow(clippy::wildcard_imports)]
use super::*;
impl JavaClient {
pub async fn handle_play_ping_request(&self, request: SPlayPingRequest) {
self.enqueue_packet(&CPingResponse::new(request.payload))
.await;
}
}

View File

@ -0,0 +1,213 @@
#[allow(clippy::wildcard_imports)]
use super::*;
impl JavaClient {
#[allow(clippy::too_many_lines)]
pub async fn handle_place_recipe(
&self,
server: &Arc<Server>,
player: &Arc<Player>,
packet: SPlaceRecipe,
) {
use crate::net::java::recipe_helper::{
GenericIngredient, compute_biggest_craftable, take_n_ingredient,
};
use crate::server::recipe::DynamicRecipe;
use pumpkin_data::recipes::{CraftingRecipeTypes, RECIPES_COOKING, RECIPES_CRAFTING};
use pumpkin_data::screen::WindowType;
use pumpkin_inventory::crafting::recipe_provider::RecipeProvider;
let target_id = packet.recipe_display_id.0 as usize;
let use_max = packet.use_max_items;
// Count crafting display IDs.
let crafting_display_count = RECIPES_CRAFTING
.iter()
.filter(|r| {
!matches!(
r,
CraftingRecipeTypes::CraftingSpecial
| CraftingRecipeTypes::CraftingDecoratedPot { .. }
)
})
.count();
let cooking_display_count = RECIPES_COOKING.len();
let dynamic_recipes = server.recipe_manager.get_dynamic_recipes().await;
let (grid_width, crafting_inv) = {
let screen_handler_arc = player.current_screen_handler.lock().await.clone();
let handler = screen_handler_arc.lock().await;
let grid_width: usize = match handler.window_type() {
Some(WindowType::Crafting) => 3,
None => 2, // player inventory 2x2
_ => return,
};
(grid_width, handler.get_behaviour().slots[1].get_inventory())
};
let grid_size = grid_width * grid_width;
let mut ingredient_slots: Vec<Option<GenericIngredient<'_>>> = vec![None; grid_size];
if target_id < crafting_display_count {
// Crafting recipe
let mut counter = 0usize;
let recipe = RECIPES_CRAFTING.iter().find(|r| {
if matches!(
r,
CraftingRecipeTypes::CraftingSpecial
| CraftingRecipeTypes::CraftingDecoratedPot { .. }
) {
return false;
}
let found = counter == target_id;
counter += 1;
found
});
let Some(recipe) = recipe else { return };
match recipe {
CraftingRecipeTypes::CraftingShaped { pattern, key, .. } => {
for (row, row_str) in pattern.iter().enumerate() {
for (col, ch) in row_str.chars().enumerate() {
if ch != ' '
&& let Some(ing) =
key.iter().find_map(|(k, v)| (*k == ch).then_some(v))
&& row * grid_width + col < grid_size
{
ingredient_slots[row * grid_width + col] =
Some(GenericIngredient::Vanilla(ing));
}
}
}
}
CraftingRecipeTypes::CraftingShapeless { ingredients, .. } => {
for (i, ing) in ingredients.iter().enumerate().take(grid_size) {
ingredient_slots[i] = Some(GenericIngredient::Vanilla(ing));
}
}
CraftingRecipeTypes::CraftingTransmute {
input, material, ..
} => {
if grid_size >= 2 {
ingredient_slots[0] = Some(GenericIngredient::Vanilla(input));
ingredient_slots[1] = Some(GenericIngredient::Vanilla(material));
}
}
_ => return,
}
} else if target_id < crafting_display_count + cooking_display_count {
// TODO: cooking recipes
return;
} else {
let dynamic_id = target_id - crafting_display_count - cooking_display_count;
let Some(DynamicRecipe::Crafting(crafting)) = dynamic_recipes.get(dynamic_id) else {
return;
};
match crafting {
pumpkin_protocol::codec::recipe::OwnedCraftingRecipe::Shaped {
pattern,
key,
..
} => {
for (row, row_str) in pattern.iter().enumerate() {
for (col, ch) in row_str.chars().enumerate() {
if ch != ' '
&& let Some((_, ing)) = key.iter().find(|(k, _)| *k == ch)
&& row * grid_width + col < grid_size
{
ingredient_slots[row * grid_width + col] =
Some(GenericIngredient::Dynamic(ing));
}
}
}
}
pumpkin_protocol::codec::recipe::OwnedCraftingRecipe::Shapeless {
ingredients,
..
} => {
for (i, ing) in ingredients.iter().enumerate().take(grid_size) {
ingredient_slots[i] = Some(GenericIngredient::Dynamic(ing));
}
}
}
}
// Check if this exact recipe is already placed (determines stacking vs fresh fill).
let recipe_matches = {
let mut ok = true;
for (idx, ing) in ingredient_slots.iter().enumerate() {
let stack = crafting_inv.get_stack(idx).await;
match ing {
None => {
if !stack.is_empty() {
ok = false;
break;
}
}
Some(ingredient) => {
if stack.is_empty() || !ingredient.match_item(stack.item) {
ok = false;
break;
}
}
}
}
ok
};
// Read minimum count from occupied slots before clearing (needed for stacking).
let current_min = if recipe_matches && !use_max {
let mut min = u8::MAX;
for (idx, ing) in ingredient_slots.iter().enumerate() {
if ing.is_some() {
let stack = crafting_inv.get_stack(idx).await;
if !stack.is_empty() {
min = min.min(stack.item_count);
}
}
}
if min == u8::MAX { 0 } else { min }
} else {
0
};
// Always clear the grid first, returning items to inventory.
for i in 0..grid_size {
let stack = crafting_inv.remove_stack(i).await;
if !stack.is_empty() {
player.inventory.offer(stack, false, player.as_ref()).await;
}
}
// Determine how many of each ingredient to place per slot.
let active_ingredients: Vec<GenericIngredient<'_>> =
ingredient_slots.iter().flatten().copied().collect();
let amount_to_craft = if use_max {
compute_biggest_craftable(&active_ingredients, &player.inventory).await
} else if recipe_matches {
current_min.saturating_add(1)
} else {
1
};
if amount_to_craft == 0 {
let screen_handler_arc = player.current_screen_handler.lock().await.clone();
screen_handler_arc.lock().await.send_content_updates().await;
return;
}
// Fill each grid slot with exactly `amount_to_craft` matching items.
for (idx, ing) in ingredient_slots.iter().enumerate() {
let Some(ingredient) = ing else { continue };
let taken = take_n_ingredient(&player.inventory, ingredient, amount_to_craft).await;
if !taken.is_empty() {
crafting_inv.set_stack(idx, taken).await;
}
}
let screen_handler_arc = player.current_screen_handler.lock().await.clone();
screen_handler_arc.lock().await.send_content_updates().await;
}
}

View File

@ -0,0 +1,34 @@
#[allow(clippy::wildcard_imports)]
use super::*;
impl JavaClient {
pub async fn handle_player_abilities(
&self,
player: &Arc<Player>,
player_abilities: SPlayerAbilities,
server: &Arc<Server>,
) {
let (flying, allow_flying) = {
let abilities = player.abilities.lock().await;
(abilities.flying, abilities.allow_flying)
};
// Set the flying ability
let new_flying = player_abilities.flags & 0x02 != 0 && allow_flying;
if flying != new_flying {
send_cancellable! {{
server;
PlayerToggleFlightEvent::new(player.clone(), new_flying);
'after: {
if event.is_flying {
player.living_entity.fall_distance.store(0.0);
}
player.abilities.lock().await.flying = event.is_flying;
}
'cancelled: {
player.send_abilities_update().await;
}
}}
}
}
}

View File

@ -0,0 +1,286 @@
#[allow(clippy::wildcard_imports)]
use super::*;
impl JavaClient {
#[expect(clippy::too_many_lines)]
pub async fn handle_player_action(
&self,
player: &Arc<Player>,
player_action: SPlayerAction,
server: &Server,
) {
if !player.has_client_loaded() {
return;
}
player.update_last_action_time();
match Status::try_from(player_action.status.0) {
Ok(status) => match status {
Status::StartedDigging => {
if !player.can_interact_with_block_at(&player_action.position, 1.0) {
warn!(
"Player {0} tried to interact with block out of reach at {1}",
player.gameprofile.name, player_action.position
);
self.update_sequence(player, player_action.sequence.0);
return;
}
let position = player_action.position;
let entity = &player.get_entity();
let world = entity.world.load_full();
let (block, state) = world.get_block_and_state(&position);
if let Some(server_arc) = world.server.upgrade() {
let mut event =
crate::plugin::api::events::block::block_damage::BlockDamageEvent::new(
player.clone(),
block,
position,
false,
);
server_arc
.plugin_manager
.fire(&server_arc, &mut event)
.await;
if event.cancelled {
self.update_sequence(player, player_action.sequence.0);
return;
}
}
if block == &pumpkin_data::Block::NOTE_BLOCK {
let props =
pumpkin_data::block_properties::NoteBlockLikeProperties::from_state_id(
state.id, block,
);
crate::block::blocks::note::NoteBlock::play_note(&props, &world, &position)
.await;
player
.increment_stat(
StatisticCategory::Custom,
CustomStatistic::PlayNoteblock as i32,
1,
)
.await;
}
let inventory = player.inventory();
let held = inventory.held_item().await;
if !server.item_registry.can_mine(held.item, player) {
self.enqueue_packet(&CBlockUpdate::new(
position,
VarInt(i32::from(state.id.as_u16())),
))
.await;
self.update_sequence(player, player_action.sequence.0);
return;
}
// TODO: do validation
// TODO: Config
if player.gamemode.load() == GameMode::Creative {
// Block break & play sound
let new_state = world
.break_block(
&position,
Some(player.clone()),
BlockFlags::NOTIFY_NEIGHBORS | BlockFlags::SKIP_DROPS,
)
.await;
if new_state.is_some() {
server
.block_registry
.broken(&world, block, player, &position, server, state)
.await;
}
self.sync_block_state_to_client(&world, position).await;
self.update_sequence(player, player_action.sequence.0);
return;
}
player.start_mining_time.store(
player.tick_counter.load(Ordering::Relaxed),
Ordering::Relaxed,
);
if !state.is_air() {
let speed = block::calc_block_breaking(player, state, block).await;
// Instant break
if speed >= 1.0 {
let broken_state = world.get_block_state(&position);
let can_harvest = player.can_harvest(broken_state, block).await;
let new_state = world
.break_block(
&position,
Some(player.clone()),
BlockFlags::NOTIFY_NEIGHBORS,
)
.await;
if new_state.is_some() {
server
.block_registry
.broken(&world, block, player, &position, server, broken_state)
.await;
player.apply_tool_damage_for_block_break(broken_state).await;
if can_harvest {
player.add_exhaustion(MINE_BLOCK_EXHAUSTION).await;
}
let item_id = player.inventory().held_item().await.item.id;
player
.increment_stat(StatisticCategory::Used, item_id as i32, 1)
.await;
player
.increment_stat(
StatisticCategory::Mined,
broken_state.id.as_u16() as i32,
1,
)
.await;
}
self.sync_block_state_to_client(&world, position).await;
} else {
player.mining.store(true, Ordering::Relaxed);
*player.mining_pos.lock().await = position;
let progress = (speed * 10.0) as i32;
player
.current_block_breaking_speed
.store(speed.to_bits(), Ordering::Relaxed);
world
.set_block_breaking(
entity,
position,
BlockBreakingProgress::Start {
stage: progress,
speed,
},
)
.await;
player
.current_block_destroy_stage
.store(progress, Ordering::Relaxed);
}
}
self.update_sequence(player, player_action.sequence.0);
}
Status::CancelledDigging => {
if !player.can_interact_with_block_at(&player_action.position, 1.0) {
warn!(
"Player {0} tried to interact with block out of reach at {1}",
player.gameprofile.name, player_action.position
);
self.update_sequence(player, player_action.sequence.0);
return;
}
player.mining.store(false, Ordering::Relaxed);
let entity = &player.get_entity();
entity
.world
.load()
.set_block_breaking(
entity,
player_action.position,
BlockBreakingProgress::Stop,
)
.await;
self.update_sequence(player, player_action.sequence.0);
}
Status::FinishedDigging => {
// TODO: do validation
let location = player_action.position;
if !player.can_interact_with_block_at(&location, 1.0) {
warn!(
"Player {0} tried to interact with block out of reach at {1}",
player.gameprofile.name, player_action.position
);
self.update_sequence(player, player_action.sequence.0);
return;
}
// Block break & play sound
let entity = &player.get_entity();
let world = entity.world.load_full();
player.mining.store(false, Ordering::Relaxed);
world
.set_block_breaking(entity, location, BlockBreakingProgress::Stop)
.await;
let (block, state) = world.get_block_and_state(&location);
let block_drop = player.gamemode.load() != GameMode::Creative
&& player.can_harvest(state, block).await;
let new_state = world
.break_block(
&location,
Some(player.clone()),
if block_drop {
BlockFlags::NOTIFY_NEIGHBORS
} else {
BlockFlags::SKIP_DROPS | BlockFlags::NOTIFY_NEIGHBORS
},
)
.await;
if new_state.is_some() {
server
.block_registry
.broken(&world, block, player, &location, server, state)
.await;
player.apply_tool_damage_for_block_break(state).await;
if block_drop {
player.add_exhaustion(MINE_BLOCK_EXHAUSTION).await;
}
let item_id = player.inventory().held_item().await.item.id;
player
.increment_stat(StatisticCategory::Used, item_id as i32, 1)
.await;
player
.increment_stat(StatisticCategory::Mined, state.id.as_u16() as i32, 1)
.await;
}
self.sync_block_state_to_client(&world, location).await;
self.update_sequence(player, player_action.sequence.0);
}
Status::DropItem => {
player.drop_held_item(false).await;
}
Status::DropItemStack => {
player.drop_held_item(true).await;
}
Status::ReleaseItemInUse => {
let item_in_use = player.living_entity.item_in_use.lock().await.clone();
if let Some(stack) = item_in_use {
server.item_registry.on_stopped_using(&stack, player).await;
}
player.living_entity.clear_active_hand().await;
}
Status::SwapItem => {
player.swap_item().await;
}
Status::SpearJab => {
debug!("todo");
}
},
Err(_) => self.kick(TextComponent::text("Invalid status")).await,
}
}
pub fn update_sequence(&self, _player: &Player, sequence: i32) {
if sequence < 0 {
error!("Expected packet sequence >= 0");
}
self.packet_sequence.store(
self.packet_sequence.load(Ordering::Relaxed).max(sequence),
Ordering::Relaxed,
);
}
async fn sync_block_state_to_client(&self, world: &World, position: BlockPos) {
let synced_state_id = world.get_block_state_id(&position);
self.send_packet_now(&CBlockUpdate::new(
position,
VarInt(i32::from(synced_state_id.as_u16())),
))
.await;
}
}

View File

@ -0,0 +1,67 @@
#[allow(clippy::wildcard_imports)]
use super::*;
impl JavaClient {
pub async fn handle_player_command(
&self,
player: &Arc<Player>,
command: SPlayerCommand,
server: &Arc<Server>,
) {
if command.entity_id != player.entity_id().into() {
return;
}
if !player.has_client_loaded() {
return;
}
player.update_last_action_time();
let entity = &player.get_entity();
match command.action {
Action::StartSprinting => {
if !entity.is_sprinting() {
send_cancellable! {{
server;
PlayerToggleSprintEvent::new(player.clone(), true);
'after: {
player.get_entity().set_sprinting(event.is_sprinting).await;
}
}}
}
}
Action::StopSprinting => {
if entity.is_sprinting() {
send_cancellable! {{
server;
PlayerToggleSprintEvent::new(player.clone(), false);
'after: {
player.get_entity().set_sprinting(event.is_sprinting).await;
}
}}
}
}
Action::LeaveBed => player.wake_up().await,
Action::StartHorseJump | Action::StopHorseJump | Action::OpenVehicleInventory => {
debug!("todo");
}
Action::StartFlyingElytra => {
let fall_flying = entity.check_fall_flying();
if entity.is_fall_flying() != fall_flying {
entity.set_fall_flying(fall_flying).await;
}
}
// <= 1.21.5
Action::StartSneaking | Action::StopSneaking => {
self.handle_player_input(
player,
SPlayerInput {
input: SPlayerInput::SNEAK,
},
server,
)
.await;
}
}
}
}

View File

@ -0,0 +1,12 @@
#[allow(clippy::wildcard_imports)]
use super::*;
impl JavaClient {
pub fn handle_player_ground(&self, player: &Player, ground: &SSetPlayerGround) {
player
.living_entity
.entity
.on_ground
.store(ground.on_ground, Ordering::Relaxed);
}
}

View File

@ -0,0 +1,52 @@
#[allow(clippy::wildcard_imports)]
use super::*;
impl JavaClient {
pub async fn handle_player_input(
&self,
player: &Arc<Player>,
input: SPlayerInput,
server: &Arc<Server>,
) {
player.last_input.store(input.input, Ordering::Relaxed);
let sneak = input.input & SPlayerInput::SNEAK != 0;
if sneak
&& player.gamemode.load() == GameMode::Spectator
&& player.camera_target_id.load().is_some()
{
player.camera_target_id.store(None);
player
.client
.send_packet_now(&CSetCamera::new(player.entity_id().into()))
.await;
}
if player.get_entity().is_sneaking() != sneak {
send_cancellable! {{
server;
PlayerToggleSneakEvent::new(player.clone(), sneak);
'after: {
player.get_entity().set_sneaking(event.is_sneaking).await;
if event.is_sneaking {
let vehicle = player.get_entity().vehicle.lock().await.clone();
if let Some(vehicle) = vehicle {
vehicle
.get_entity()
.remove_passenger(player.entity_id())
.await;
}
}
}
}}
} else if sneak {
let vehicle = player.get_entity().vehicle.lock().await.clone();
if let Some(vehicle) = vehicle {
vehicle
.get_entity()
.remove_passenger(player.entity_id())
.await;
}
}
}
}

View File

@ -0,0 +1,8 @@
#[allow(clippy::wildcard_imports)]
use super::*;
impl JavaClient {
pub fn handle_player_loaded(player: &Player) {
player.set_client_loaded(true);
}
}

View File

@ -0,0 +1,347 @@
#[allow(clippy::wildcard_imports)]
use super::*;
impl JavaClient {
const fn clamp_horizontal(pos: f64) -> f64 {
pos.clamp(-3.0E7, 3.0E7)
}
const fn clamp_vertical(pos: f64) -> f64 {
pos.clamp(-2.0E7, 2.0E7)
}
/// Returns whether syncing the position was needed
fn sync_position(
player: &Arc<Player>,
world: &World,
pos: Vector3<f64>,
last_pos: Vector3<f64>,
yaw: f32,
pitch: f32,
on_ground: bool,
) -> bool {
let delta = Vector3::new(pos.x - last_pos.x, pos.y - last_pos.y, pos.z - last_pos.z);
let entity_id = player.entity_id();
// Teleport when more than 8 blocks (-8..=7.999755859375)
if delta.length_squared() < 64.0 {
return false;
}
// Sync position with all other players.
world.broadcast_packet_except(
&[player.gameprofile.id],
&CEntityPositionSync::new(
entity_id.into(),
pos,
Vector3::new(0.0, 0.0, 0.0),
yaw,
pitch,
on_ground,
),
);
true
}
#[expect(clippy::too_many_lines)]
pub async fn handle_position(
&self,
player: &Arc<Player>,
server: &Arc<Server>,
packet: SPlayerPosition,
) {
if !player.has_client_loaded() {
return;
}
if player.get_entity().has_vehicle().await {
return;
}
// Ignore movement packets while awaiting a teleport confirmation (vanilla behavior)
if player.awaiting_teleport.lock().await.is_some() {
return;
}
// y = feet Y
let position = packet.position;
if position.x.is_nan() || position.y.is_nan() || position.z.is_nan() {
self.kick(TextComponent::translate_cross(
translation::java::MULTIPLAYER_DISCONNECT_INVALID_PLAYER_MOVEMENT,
translation::java::MULTIPLAYER_DISCONNECT_INVALID_PLAYER_MOVEMENT,
[],
))
.await;
return;
}
let position = Vector3::new(
Self::clamp_horizontal(position.x),
Self::clamp_vertical(position.y),
Self::clamp_horizontal(position.z),
);
send_cancellable! {{
server;
PlayerMoveEvent {
player: player.clone(),
from: player.get_entity().pos.load(),
to: position,
cancelled: false,
};
'after: {
let pos = event.to;
let entity = &player.get_entity();
let last_pos = entity.pos.load();
player.get_entity().set_pos(pos);
let distance = last_pos.squared_distance_to_vec(&pos).sqrt();
let cm = (distance * 100.0) as i32;
if cm > 0 {
let stat = player.get_movement_statistic().await;
player
.increment_stat(StatisticCategory::Custom, stat as i32, cm)
.await;
}
let height_difference = pos.y - last_pos.y;
if entity.on_ground.load(Ordering::Relaxed) && packet.collision & FLAG_ON_GROUND == 0 && height_difference > 0.0 {
player.jump().await;
}
let new_on_ground = packet.collision & FLAG_ON_GROUND != 0;
entity.on_ground.store(new_on_ground, Ordering::Relaxed);
if new_on_ground && entity.is_fall_flying() {
entity.set_fall_flying(false).await;
}
let world = &player.world();
// TODO: Warn when player moves to quickly
if !Self::sync_position(player, world, pos, last_pos, entity.yaw.load(), entity.pitch.load(), packet.collision & FLAG_ON_GROUND != 0) {
// Send the new position to all other players.
world.broadcast_packet_except_editioned_sync(
&[player.gameprofile.id],
&CUpdateEntityPos::new(
player.entity_id().into(),
Vector3::new(
pos.x.mul_add(4096.0, -(last_pos.x * 4096.0)) as i16,
pos.y.mul_add(4096.0, -(last_pos.y * 4096.0)) as i16,
pos.z.mul_add(4096.0, -(last_pos.z * 4096.0)) as i16,
),
packet.collision & FLAG_ON_GROUND != 0,
),
&CMovePlayer::new(
VarULong(player.entity_id() as u64),
Vector3::new(pos.x as f32, pos.y as f32 + player.get_entity().entity_type.eye_height, pos.z as f32),
entity.pitch.load(),
entity.yaw.load(),
entity.yaw.load(),
CMovePlayer::MODE_NORMAL,
(packet.collision & FLAG_ON_GROUND) != 0,
VarULong(0),
0,
0,
VarULong(0),
),
);
}
// Only process fall damage if player is alive
if !player.abilities.lock().await.flying
&& player.living_entity.health.load() > 0.0
&& !player.living_entity.dead.load(Ordering::Relaxed)
{
player.living_entity
.fall(
player.clone(),
height_difference,
packet.collision & FLAG_ON_GROUND != 0,
player.gamemode.load() == GameMode::Creative,
)
.await;
}
chunker::update_position(player).await;
let delta = Vector3::new(
pos.x - last_pos.x,
pos.y - last_pos.y,
pos.z - last_pos.z,
);
// Only update idle timeout if there's actual movement (vanilla threshold)
if delta.length_squared() > 1.0E-5 {
player.update_last_action_time();
}
player.progress_motion(delta).await;
}
'cancelled: {
self.force_tp(player, player.get_entity().pos.load()).await;
}
}}
}
#[expect(clippy::too_many_lines)]
pub async fn handle_position_rotation(
&self,
player: &Arc<Player>,
server: &Arc<Server>,
packet: SPlayerPositionRotation,
) {
if !player.has_client_loaded() {
return;
}
if player.get_entity().has_vehicle().await {
return;
}
// Ignore movement packets while awaiting a teleport confirmation (vanilla behavior)
if player.awaiting_teleport.lock().await.is_some() {
return;
}
// y = feet Y
let position = packet.position;
if !position.x.is_finite()
|| !position.y.is_finite()
|| !position.z.is_finite()
|| !packet.yaw.is_finite()
|| !packet.pitch.is_finite()
{
self.kick(TextComponent::translate_cross(
translation::java::MULTIPLAYER_DISCONNECT_INVALID_PLAYER_MOVEMENT,
translation::java::MULTIPLAYER_DISCONNECT_INVALID_PLAYER_MOVEMENT,
[],
))
.await;
return;
}
let position = Vector3::new(
Self::clamp_horizontal(position.x),
Self::clamp_vertical(position.y),
Self::clamp_horizontal(position.z),
);
send_cancellable! {{
server;
PlayerMoveEvent::new(
player.clone(),
player.get_entity().pos.load(),
position,
);
'after: {
let pos = event.to;
let entity = &player.get_entity();
let last_pos = entity.pos.load();
player.get_entity().set_pos(pos);
let distance = last_pos.squared_distance_to_vec(&pos).sqrt();
let cm = (distance * 100.0) as i32;
if cm > 0 {
let stat = player.get_movement_statistic().await;
player
.increment_stat(StatisticCategory::Custom, stat as i32, cm)
.await;
}
let height_difference = pos.y - last_pos.y;
if entity.on_ground.load(Ordering::Relaxed)
&& (packet.collision & FLAG_ON_GROUND) != 0
&& height_difference > 0.0
{
player.jump().await;
}
entity
.on_ground
.store((packet.collision & FLAG_ON_GROUND) != 0, Ordering::Relaxed);
entity.set_rotation(wrap_degrees(packet.yaw) % 360.0, wrap_degrees(packet.pitch));
let entity_id = entity.entity_id;
let yaw = (entity.yaw.load() * 256.0 / 360.0).rem_euclid(256.0);
let pitch = (entity.pitch.load() * 256.0 / 360.0).rem_euclid(256.0);
// let head_yaw = (entity.head_yaw * 256.0 / 360.0).floor();
let world = entity.world.load_full();
// TODO: Warn when player moves to quickly
if !Self::
sync_position(player, &world, pos, last_pos, yaw, pitch, (packet.collision & FLAG_ON_GROUND) != 0)
{
// Send the new position to all other players.
world.broadcast_packet_except_editioned_sync(
&[player.gameprofile.id],
&CUpdateEntityPosRot::new(
entity_id.into(),
Vector3::new(
pos.x.mul_add(4096.0, -(last_pos.x * 4096.0)) as i16,
pos.y.mul_add(4096.0, -(last_pos.y * 4096.0)) as i16,
pos.z.mul_add(4096.0, -(last_pos.z * 4096.0)) as i16,
),
yaw as u8,
pitch as u8,
(packet.collision & FLAG_ON_GROUND) != 0,
),
&CMovePlayer::new(
VarULong(entity_id as u64),
Vector3::new(pos.x as f32, pos.y as f32 + player.get_entity().entity_type.eye_height, pos.z as f32),
entity.pitch.load(),
entity.yaw.load(),
entity.yaw.load(),
CMovePlayer::MODE_NORMAL,
(packet.collision & FLAG_ON_GROUND) != 0,
VarULong(0),
0,
0,
VarULong(0),
),
);
}
world
.broadcast_packet_except(
&[player.gameprofile.id],
&CHeadRot::new(entity_id.into(), yaw as u8),
)
;
// Only process fall damage if player is alive
if !player.abilities.lock().await.flying
&& player.living_entity.health.load() > 0.0
&& !player.living_entity.dead.load(Ordering::Relaxed)
{
player.living_entity
.fall(
player.clone(),
height_difference,
(packet.collision & FLAG_ON_GROUND) != 0,
player.gamemode.load() == GameMode::Creative,
)
.await;
}
chunker::update_position(player).await;
let delta = Vector3::new(
pos.x - last_pos.x,
pos.y - last_pos.y,
pos.z - last_pos.z,
);
// Only update idle timeout if there's actual movement (vanilla threshold)
if delta.length_squared() > 1.0E-5 {
player.update_last_action_time();
}
player.progress_motion(delta).await;
}
'cancelled: {
self.force_tp(player, position).await;
}
}}
}
pub async fn force_tp(&self, player: &Arc<Player>, position: Vector3<f64>) {
let teleport_id = player.teleport_id_count.fetch_add(1, Ordering::Relaxed) + 1;
*player.awaiting_teleport.lock().await = Some((teleport_id.into(), position));
self.enqueue_packet(&CPlayerPosition::new(
teleport_id.into(),
player.get_entity().pos.load(),
Vector3::new(0.0, 0.0, 0.0),
player.get_entity().yaw.load(),
player.get_entity().pitch.load(),
Vec::new(),
))
.await;
}
}

View File

@ -0,0 +1,63 @@
#[allow(clippy::wildcard_imports)]
use super::*;
impl JavaClient {
pub async fn handle_rotation(&self, player: &Player, rotation: SPlayerRotation) {
if !player.has_client_loaded() {
return;
}
if !rotation.yaw.is_finite() || !rotation.pitch.is_finite() {
self.kick(TextComponent::translate_cross(
translation::java::MULTIPLAYER_DISCONNECT_INVALID_PLAYER_MOVEMENT,
translation::java::MULTIPLAYER_DISCONNECT_INVALID_PLAYER_MOVEMENT,
[],
))
.await;
return;
}
let entity = &player.get_entity();
entity.on_ground.store(rotation.ground, Ordering::Relaxed);
entity.set_rotation(
wrap_degrees(rotation.yaw) % 360.0,
wrap_degrees(rotation.pitch),
);
// Send the new position to all other players.
let entity_id = entity.entity_id;
let yaw = (entity.yaw.load() * 256.0 / 360.0).rem_euclid(256.0);
let pitch = (entity.pitch.load() * 256.0 / 360.0).rem_euclid(256.0);
// let head_yaw = modulus(entity.head_yaw * 256.0 / 360.0, 256.0);
let world = entity.world.load_full();
let je_packet =
CUpdateEntityRot::new(entity_id.into(), yaw as u8, pitch as u8, rotation.ground);
let pos = entity.pos.load();
let be_packet = CMovePlayer::new(
VarULong(entity_id as u64),
Vector3::new(
pos.x as f32,
pos.y as f32 + player.get_entity().entity_type.eye_height,
pos.z as f32,
),
entity.pitch.load(),
entity.yaw.load(),
entity.yaw.load(),
CMovePlayer::MODE_ROTATION,
rotation.ground,
VarULong(0),
0,
0,
VarULong(0),
);
world.broadcast_packet_except_editioned_sync(
&[player.gameprofile.id],
&je_packet,
&be_packet,
);
let je_packet = CHeadRot::new(entity_id.into(), yaw as u8);
world.broadcast_packet_except(&[player.gameprofile.id], &je_packet);
}
}

View File

@ -0,0 +1,13 @@
#[allow(clippy::wildcard_imports)]
use super::*;
impl JavaClient {
#[allow(clippy::unused_async)]
pub async fn handle_recipe_book_change_settings(
&self,
_player: &Arc<Player>,
_packet: SRecipeBookChangeSettings,
) {
// Client is updating its recipe book filter/open state; no server action needed.
}
}

View File

@ -0,0 +1,13 @@
#[allow(clippy::wildcard_imports)]
use super::*;
impl JavaClient {
#[allow(clippy::unused_async)]
pub async fn handle_recipe_book_seen_recipe(
&self,
_player: &Arc<Player>,
_packet: SRecipeBookSeenRecipe,
) {
// Client acknowledged a recipe display; no server action needed.
}
}

View File

@ -0,0 +1,18 @@
#[allow(clippy::wildcard_imports)]
use super::*;
impl JavaClient {
pub async fn handle_seen_advancement(&self, player: &Arc<Player>, packet: SSeenAdvancement) {
if let SSeenAdvancement::OpenTab(tab) = packet {
let advancement = Advancement::from_minecraft_name(&tab.to_string());
if advancement.is_some() {
player
.advancements
.lock()
.await
.set_selected_tab(advancement)
.await;
}
}
}
}

View File

@ -0,0 +1,17 @@
#[allow(clippy::wildcard_imports)]
use super::*;
impl JavaClient {
pub async fn handle_select_trade(&self, player: &Arc<Player>, packet: SSelectTrade) {
let screen_handler = player.current_screen_handler.lock().await;
let mut screen_handler = screen_handler.lock().await;
if let Some(merchant) = screen_handler
.as_any_mut()
.downcast_mut::<MerchantScreenHandler>()
{
merchant
.set_selected_offer(packet.selected_slot.0 as usize)
.await;
}
}
}

View File

@ -0,0 +1,97 @@
#[allow(clippy::wildcard_imports)]
use super::*;
impl JavaClient {
pub async fn handle_set_command_block(
&self,
player: &Arc<Player>,
command: SSetCommandBlock<'_>,
) {
if !player.is_creative() {
return;
}
if player.permission_lvl.load() < PermissionLvl::Two {
return;
}
let pos = command.pos;
let block_entity = player.world().get_block_entity(&pos);
if let Some(block_entity) = block_entity {
if block_entity.resource_location() != CommandBlockEntity::ID {
warn!("Client tried to change Command block but not Command block entity found");
return;
}
let Ok(command_block_mode) = CommandBlockMode::try_from(command.mode) else {
self.kick(TextComponent::text("Invalid Command block mode"))
.await;
return;
};
let block = player.world().get_block(&pos);
let old_state_id = player.world().get_block_state_id(&pos);
let mut props = CommandBlockLikeProperties::from_state_id(old_state_id, block);
let block_type = match command_block_mode {
CommandBlockMode::Chain => Block::CHAIN_COMMAND_BLOCK,
CommandBlockMode::Repeating => Block::REPEATING_COMMAND_BLOCK,
CommandBlockMode::Impulse => Block::COMMAND_BLOCK,
};
let Some(old_command_block) =
block_entity.as_any().downcast_ref::<CommandBlockEntity>()
else {
return;
};
props.conditional = command.flags & 0x2 != 0;
let new_state_id = props.to_state_id(&block_type);
player
.world()
.set_block_state(
&command.pos,
new_state_id,
BlockFlags::SKIP_BLOCK_ADDED_CALLBACK,
)
.await;
let mut cmd = command.command;
if cmd.starts_with('/') {
cmd = &cmd[1..];
}
let command_block = CommandBlockEntity {
position: pos,
powered: old_command_block.powered.load(Ordering::SeqCst).into(),
condition_met: old_command_block
.condition_met
.load(Ordering::SeqCst)
.into(),
auto: (command.flags & 0x4 != 0).into(),
dirty: old_command_block.dirty.load(Ordering::SeqCst).into(),
command: Mutex::new(cmd.to_string()),
last_output: old_command_block.last_output.lock().await.clone().into(),
track_output: (command.flags & 0x1 != 0).into(),
success_count: AtomicU32::new(0),
};
player.world().add_block_entity(Arc::new(command_block));
player
.send_system_message(&TextComponent::text(format!(
"Command set: {}",
command.command
)))
.await;
// The 0x4 flag means always active
if command.flags & 0x4 != 0 && block_type != Block::CHAIN_COMMAND_BLOCK {
player.world().schedule_block_tick(
&block_type,
pos,
1,
pumpkin_world::tick::TickPriority::Normal,
);
}
}
}
}

View File

@ -0,0 +1,68 @@
#[allow(clippy::wildcard_imports)]
use super::*;
impl JavaClient {
pub async fn handle_set_creative_slot(
&self,
player: &Player,
packet: SSetCreativeSlot,
) -> Result<(), InventoryError> {
if player.gamemode.load() != GameMode::Creative {
return Err(InventoryError::PermissionError);
}
let is_negative = packet.slot < 0;
let valid_slot = packet.slot >= 1 && packet.slot as usize <= 45;
let item_stack = packet
.clicked_item
.to_stack_for_version(&self.version.load());
let is_legal =
item_stack.is_empty() || item_stack.item_count <= item_stack.get_max_stack_size();
if valid_slot && is_legal {
let mut player_screen_handler = player.player_screen_handler.lock().await;
let is_armor_equipped = player_screen_handler
.get_slot(packet.slot as usize)
.get_stack()
.await
.are_equal(&item_stack);
if !is_armor_equipped {
if (5..9).contains(&packet.slot) {
player
.enqueue_equipment_change(
&match packet.slot {
5 => EquipmentSlot::HEAD,
6 => EquipmentSlot::CHEST,
7 => EquipmentSlot::LEGS,
8 => EquipmentSlot::FEET,
_ => {
tracing::error!("Invalid armor slot: {}", packet.slot);
EquipmentSlot::HEAD
}
},
&item_stack,
)
.await;
} else if (36..45).contains(&packet.slot) {
let slot = packet.slot - 36;
if player.inventory().get_selected_slot() == slot as u8 {
let equipment = &[(EquipmentSlot::MAIN_HAND, item_stack.clone())];
player.living_entity.send_equipment_changes(equipment);
}
}
}
player_screen_handler
.get_slot(packet.slot as usize)
.set_stack(item_stack.clone())
.await;
player_screen_handler.set_received_stack(packet.slot as usize, item_stack);
player_screen_handler.send_content_updates().await;
drop(player_screen_handler);
} else if is_negative && is_legal {
// Item drop
player.drop_item(item_stack).await;
}
Ok(())
}
}

View File

@ -0,0 +1,39 @@
#[allow(clippy::wildcard_imports)]
use super::*;
impl JavaClient {
pub async fn handle_set_held_item(
&self,
server: &Arc<Server>,
player: &Player,
held: SSetHeldItem,
) {
player.update_last_action_time();
let slot = held.slot;
if !(0..=8).contains(&slot) {
self.kick(TextComponent::text("Invalid held slot")).await;
return;
}
let slot = slot as u8;
let previous_slot = player.inventory.get_selected_slot();
let Some(player_arc) = player.world().get_player_by_uuid(player.gameprofile.id) else {
return;
};
let mut event = PlayerItemHeldEvent::new(player_arc, previous_slot, slot);
server.plugin_manager.fire(server, &mut event).await;
if event.cancelled {
player
.client
.enqueue_packet(&CSetSelectedSlot::new(previous_slot as i8))
.await;
return;
}
let inv = player.inventory();
inv.set_selected_slot(slot);
let stack = inv.held_item().await;
let equipment = &[(EquipmentSlot::MAIN_HAND, stack)];
player.living_entity.send_equipment_changes(equipment);
}
}

View File

@ -0,0 +1,40 @@
#[allow(clippy::wildcard_imports)]
use super::*;
impl JavaClient {
pub async fn handle_set_jigsaw_block(&self, player: &Arc<Player>, jigsaw: SSetJigsawBlock<'_>) {
if !player.is_creative() {
return;
}
if player.permission_lvl.load() < PermissionLvl::Two {
return;
}
let pos = jigsaw.pos;
let block_entity = player.world().get_block_entity(&pos);
if let Some(block_entity) = block_entity {
if block_entity.resource_location() != JigsawBlockEntity::ID {
warn!("Client tried to change Jigsaw block but not Jigsaw block entity found");
return;
}
let Some(jigsaw_block) = block_entity.as_any().downcast_ref::<JigsawBlockEntity>()
else {
return;
};
*jigsaw_block.name.lock().await = jigsaw.name.to_string();
*jigsaw_block.target.lock().await = jigsaw.target.to_string();
*jigsaw_block.pool.lock().await = jigsaw.pool.to_string();
*jigsaw_block.final_state.lock().await = jigsaw.final_state.to_string();
*jigsaw_block.joint.lock().await = JigsawJointType::from_str(jigsaw.joint);
jigsaw_block
.selection_priority
.store(jigsaw.selection_priority.0, Ordering::SeqCst);
jigsaw_block
.placement_priority
.store(jigsaw.placement_priority.0, Ordering::SeqCst);
player.world().update_block_entity(&block_entity);
}
}
}

View File

@ -0,0 +1,15 @@
#[allow(clippy::wildcard_imports)]
use super::*;
impl JavaClient {
pub fn handle_set_test_block(&self, player: &Arc<Player>, packet: &SSetTestBlock<'_>) {
if !player.has_client_loaded() {
return;
}
player.update_last_action_time();
debug!(
"Set test block at {:?}: mode={:?}, message={}",
packet.position, packet.mode, packet.message
);
}
}

View File

@ -0,0 +1,63 @@
#[allow(clippy::wildcard_imports)]
use super::*;
impl JavaClient {
pub async fn handle_swing_arm(
&self,
server: &Arc<Server>,
player: &Arc<Player>,
swing_arm: SSwingArm,
) {
player.update_last_action_time();
let Ok(hand) = Hand::from_packet_id(swing_arm.hand.0) else {
self.kick(TextComponent::text("Invalid hand")).await;
return;
};
let mut anim_event = crate::plugin::api::events::player::player_animation::PlayerAnimationEvent::new(
player.clone(),
match hand {
Hand::Left => crate::plugin::api::events::player::player_animation::PlayerAnimationType::ArmSwingOff,
Hand::Right => crate::plugin::api::events::player::player_animation::PlayerAnimationType::ArmSwingMain,
},
);
server.plugin_manager.fire(server, &mut anim_event).await;
if anim_event.cancelled {
return;
}
let (yaw, pitch) = player.rotation();
let hit_result = player
.world()
.raycast(
player.eye_position(),
player
.eye_position()
.add(&(Vector3::rotation_vector(f64::from(pitch), f64::from(yaw)) * 4.5)),
async |pos, world| {
let block = world.get_block(pos);
block != &Block::AIR && block != &Block::WATER && block != &Block::LAVA
},
)
.await;
let event = if let Some((hit_pos, _hit_dir)) = hit_result {
PlayerInteractEvent::new(
player,
InteractAction::LeftClickBlock,
player.world().get_block(&hit_pos),
Some(hit_pos),
)
} else {
PlayerInteractEvent::new(player, InteractAction::LeftClickAir, &Block::AIR, None)
};
send_cancellable! {{
&server;
event;
'after: {
player.swing_hand(hand, false).await;
}
}}
}
}

View File

@ -0,0 +1,37 @@
#[allow(clippy::wildcard_imports)]
use super::*;
impl JavaClient {
pub async fn handle_teleport_to_entity(
&self,
player: &Arc<Player>,
packet: STeleportToEntity,
server: &Server,
) {
if !player.has_client_loaded() {
return;
}
player.update_last_action_time();
if player.gamemode.load() != GameMode::Spectator {
return;
}
if let Some(target_player) = server.get_player_by_uuid(packet.target) {
let target_pos = target_player.living_entity.entity.pos.load();
let target_yaw = target_player.living_entity.entity.yaw.load();
let target_pitch = target_player.living_entity.entity.pitch.load();
let target_id = target_player.living_entity.entity.entity_id;
player.camera_target_id.store(Some(target_id));
player
.client
.send_packet_now(&CSetCamera::new(target_id.into()))
.await;
player
.request_teleport(target_pos, target_yaw, target_pitch)
.await;
}
}
}

View File

@ -0,0 +1,19 @@
#[allow(clippy::wildcard_imports)]
use super::*;
impl JavaClient {
pub fn handle_test_instance_block_action(
&self,
player: &Arc<Player>,
packet: &STestInstanceBlockAction<'_>,
) {
if !player.has_client_loaded() {
return;
}
player.update_last_action_time();
debug!(
"Test instance block action at {:?}: action={:?}",
packet.pos, packet.action
);
}
}

View File

@ -0,0 +1,35 @@
#[allow(clippy::wildcard_imports)]
use super::*;
impl JavaClient {
pub async fn handle_sign_update(&self, player: &Player, sign_data: SUpdateSign<'_>) {
let world = player.get_entity().world.load_full();
let Some(block_entity) = world.get_block_entity(&sign_data.location) else {
return;
};
let Some(sign_entity) = block_entity.as_any().downcast_ref::<SignBlockEntity>() else {
return;
};
if sign_entity.is_waxed.load(Ordering::Relaxed) {
return;
}
let text = if sign_data.is_front_text {
&sign_entity.front_text
} else {
&sign_entity.back_text
};
*text
.messages
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = [
sign_data.line_1.into(),
sign_data.line_2.into(),
sign_data.line_3.into(),
sign_data.line_4.into(),
];
*sign_entity.currently_editing_player.lock().await = None;
world.update_block_entity(&block_entity);
}
}

View File

@ -0,0 +1,163 @@
#[allow(clippy::wildcard_imports)]
use super::*;
impl JavaClient {
pub async fn handle_use_item(
&self,
player: &Arc<Player>,
use_item: &SUseItem,
server: &Arc<Server>,
) {
if !player.has_client_loaded() {
return;
}
player.update_last_action_time();
let inventory = player.inventory();
let Ok(hand) = Hand::from_packet_id(use_item.hand.0) else {
self.kick(TextComponent::text("InvalidHand")).await;
return;
};
self.update_sequence(player, use_item.sequence.0);
let mut item_in_hand = inventory.get_stack_in_hand(hand).await;
let (item_id, _item) = (item_in_hand.item.id, item_in_hand.item);
player
.increment_stat(StatisticCategory::Used, item_id as i32, 1)
.await;
let hit_result = player
.world()
.raycast(
player.eye_position(),
player.eye_position().add(
&(Vector3::rotation_vector(f64::from(use_item.pitch), f64::from(use_item.yaw))
* 4.5),
),
async |pos, world| {
let block = world.get_block(pos);
block != &Block::AIR && block != &Block::WATER && block != &Block::LAVA
},
)
.await;
let event = if let Some((hit_pos, _hit_dir)) = hit_result {
PlayerInteractEvent::new(
player,
InteractAction::RightClickBlock,
player.world().get_block(&hit_pos),
Some(hit_pos),
)
} else {
PlayerInteractEvent::new(player, InteractAction::RightClickAir, &Block::AIR, None)
};
let (item_for_use, stack_for_use) = (item_in_hand.item, item_in_hand.clone());
self.prepare_hand_item_for_use(player, hand, &mut item_in_hand)
.await;
if !self
.should_continue_use_after_fish_event(server, player, hand, item_for_use)
.await
{
return;
}
send_cancellable! {{
server;
event;
'after: {
server.item_registry.on_use(&stack_for_use, player).await;
}
}}
}
async fn prepare_hand_item_for_use(
&self,
player: &Arc<Player>,
hand: Hand,
held: &mut ItemStack,
) {
let inventory = player.inventory();
if let Some(cooldown) = held.get_use_cooldown() {
let group = cooldown
.cooldown_group
.clone()
.unwrap_or_else(|| held.item.registry_key.to_string());
if player.is_on_cooldown(&group).await {
return;
}
}
if held.get_data_component::<ConsumableImpl>().is_some()
|| held.get_data_component::<BlocksAttacksImpl>().is_some()
{
// If its food we want to make sure we can actually consume it
if let Some(food) = held.get_data_component::<FoodImpl>() {
if player.abilities.lock().await.invulnerable
|| food.can_always_eat
|| player.hunger_manager.level.load() < 20
{
player
.living_entity
.set_active_hand(hand, held.clone(), held.get_max_use_time())
.await;
}
} else {
player
.living_entity
.set_active_hand(hand, held.clone(), held.get_max_use_time())
.await;
}
}
let equipment_slot = held
.get_data_component::<EquippableImpl>()
.map(|equippable| equippable.slot.clone());
if let Some(slot) = equipment_slot {
// The equipment lock has to be released before touching the hand again:
// the off hand lives in the same map, so holding it here would deadlock.
let current_equipped = inventory.entity_equipment.lock().await.get(&slot);
if current_equipped.are_items_and_components_equal(held) {
return;
}
player.enqueue_equipment_change(&slot, held).await;
let equipped = if current_equipped.is_empty() {
let equipped = held.clone();
held.decrement_unless_creative(player.gamemode.load(), 1);
equipped
} else {
std::mem::replace(held, current_equipped)
};
inventory.entity_equipment.lock().await.put(&slot, equipped);
inventory.set_stack_in_hand(hand, held.clone()).await;
}
}
async fn should_continue_use_after_fish_event(
&self,
server: &Arc<Server>,
player: &Arc<Player>,
hand: Hand,
item_for_use: &Item,
) -> bool {
if item_for_use.id != Item::FISHING_ROD.id {
return true;
}
// TODO: Apply fishing rod durability on retrieval based on catch type.
let mut fish_event = PlayerFishEvent::new(
player.clone(),
None,
uuid::Uuid::nil(),
String::new(),
PlayerFishState::Fishing,
hand,
0,
);
server.plugin_manager.fire(server, &mut fish_event).await;
!fish_event.cancelled
}
}

View File

@ -0,0 +1,262 @@
#[allow(clippy::wildcard_imports)]
use super::*;
impl JavaClient {
#[allow(clippy::too_many_lines)]
pub async fn handle_use_item_on(
&self,
player: &Arc<Player>,
use_item_on: SUseItemOn,
server: &Arc<Server>,
) -> Result<(), BlockPlacingError> {
if !player.has_client_loaded() {
return Ok(());
}
player.update_last_action_time();
self.update_sequence(player, use_item_on.sequence.0);
let position = use_item_on.position;
let cursor_pos = use_item_on.cursor_pos;
let mut should_try_decrement = false;
if !player.can_interact_with_block_at(&position, 1.0) {
// TODO: maybe log?
return Err(BlockPlacingError::BlockOutOfReach);
}
let Ok(face) = BlockDirection::try_from(use_item_on.face.0) else {
return Err(BlockPlacingError::InvalidBlockFace);
};
let Ok(hand) = Hand::from_packet_id(use_item_on.hand.0) else {
return Err(BlockPlacingError::InvalidHand);
};
if player.gamemode.load() == GameMode::Spectator {
// TODO: openMenu
return Ok(());
}
let inventory = player.inventory();
let held_item = inventory.held_item().await;
let off_hand_item = inventory.off_hand_item().await;
let held_item_empty = held_item.is_empty();
let off_hand_item_empty = off_hand_item.is_empty();
let mut item = inventory.get_stack_in_hand(hand).await;
let item_id = item.item.id;
player
.increment_stat(StatisticCategory::Used, item_id as i32, 1)
.await;
let entity = &player.get_entity();
let world = entity.world.load_full();
let block = world.get_block(&position);
let event = PlayerInteractEvent::new(
player,
InteractAction::RightClickBlock,
block,
Some(position),
);
send_cancellable! {{
server;
event;
'cancelled: {
let state_id = world.get_block_state_id(&position);
self.enqueue_packet(&CBlockUpdate::new(
position,
VarInt(i32::from(state_id.as_u16())),
))
.await;
return Ok(());
}
}}
let equipment_slot = if matches!(hand, Hand::Right) {
EquipmentSlot::MAIN_HAND
} else {
EquipmentSlot::OFF_HAND
};
let sneaking = player.get_entity().is_sneaking();
// Code based on the java class ServerPlayerInteractionManager
if !(sneaking && (!held_item_empty || !off_hand_item_empty)) {
let result = self
.call_use_item_on(
player,
&position,
&cursor_pos,
&face,
&mut item,
&equipment_slot,
&world,
block,
server,
)
.await;
if result.consumes_action() {
// TODO: Trigger ANY_BLOCK_USE Criteria
if matches!(result, BlockActionResult::SuccessServer) {
player.swing_hand(hand, true).await;
}
return Ok(());
}
}
let slot_index = if matches!(hand, Hand::Right) {
inventory.get_selected_slot() as usize
} else {
PlayerInventory::OFF_HAND_SLOT
};
if item.is_empty() {
// TODO item cool down
// If the hand is empty we stop here
return Ok(());
}
let before = item.clone();
server
.item_registry
.use_on_block(&mut item, player, position, face, cursor_pos, block, server)
.await;
// Check if the item is a block, because not every item can be placed :D
let item_id = item.item.id;
if let Some(block) = Block::from_item_id(item_id) {
should_try_decrement = self
.run_is_block_place(player, block, server, use_item_on, position, face)
.await?;
}
if should_try_decrement {
// TODO: Config
// Decrease block count
if player.gamemode.load() != GameMode::Creative {
item.decrement(1);
}
}
let after = item.clone();
// Broadcast the break entity status before the slot sync; the client
// needs the old item texture in the slot for break particles.
if !before.is_empty() && after.is_empty() {
let slot = if slot_index == player.inventory.get_selected_slot() as usize {
&EquipmentSlot::MAIN_HAND
} else {
&EquipmentSlot::OFF_HAND
};
player.world().send_entity_status(
player.get_entity(),
equipment_break_status(slot),
None,
);
}
if !after.are_equal(&before) {
player.sync_hand_slot(slot_index, after.clone()).await;
inventory.set_stack_in_hand(hand, after).await;
}
Ok(())
}
#[expect(clippy::too_many_arguments)]
async fn call_use_item_on(
&self,
player: &Arc<Player>,
position: &BlockPos,
cursor_pos: &Vector3<f32>,
face: &BlockDirection,
held_item: &mut ItemStack,
equipment_slot: &EquipmentSlot,
world: &Arc<World>,
block: &Block,
server: &Arc<Server>,
) -> BlockActionResult {
let result = server
.block_registry
.use_with_item(
block,
player,
position,
&BlockHitResult { face, cursor_pos },
held_item,
equipment_slot,
server,
world,
)
.await;
if result.consumes_action() {
// TODO: Trigger ITEM_USED_ON_BLOCK Criteria
return result;
}
if matches!(result, BlockActionResult::PassToDefaultBlockAction) {
let result = server
.block_registry
.on_use(
block,
player,
position,
&BlockHitResult { face, cursor_pos },
server,
world,
)
.await;
if result.consumes_action() {
// TODO: Trigger DEFAULT_BLOCK_USE Criteria
return result;
}
}
BlockActionResult::Pass
}
async fn run_is_block_place(
&self,
player: &Arc<Player>,
block: &'static Block,
server: &Arc<Server>,
use_item_on: SUseItemOn,
location: BlockPos,
face: BlockDirection,
) -> Result<bool, BlockPlacingError> {
match server
.block_registry
.place_block(player, block, server, &use_item_on, location, face)
.await
{
Ok(Some((final_block_pos, new_state))) => {
self.send_packet_now(&CBlockUpdate::new(
final_block_pos,
VarInt(i32::from(new_state.as_u16())),
))
.await;
Ok(true)
}
Ok(None) => Ok(false),
Err(crate::block::registry::BlockPlacingError::InvalidGamemode) => {
Err(BlockPlacingError::InvalidGamemode)
}
Err(crate::block::registry::BlockPlacingError::BlockOutOfWorld) => {
Err(BlockPlacingError::BlockOutOfWorld)
}
}
}
/// Checks if the block placed was a sign, then opens a dialog.
pub async fn send_sign_packet(&self, block_position: BlockPos, is_front_text: bool) {
self.enqueue_packet(&COpenSignEditor::new(block_position, is_front_text))
.await;
}
}