chore: split packets into separate folders
This commit is contained in:
parent
05c692985f
commit
0d5f868db6
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
126
crates/pumpkin/src/net/bedrock/login/login.rs
Normal file
126
crates/pumpkin/src/net/bedrock/login/login.rs
Normal 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))
|
||||||
|
}
|
||||||
|
}
|
||||||
89
crates/pumpkin/src/net/bedrock/login/mod.rs
Normal file
89
crates/pumpkin/src/net/bedrock/login/mod.rs
Normal 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;
|
||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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
32
crates/pumpkin/src/net/bedrock/play/animate.rs
Normal file
32
crates/pumpkin/src/net/bedrock/play/animate.rs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
111
crates/pumpkin/src/net/bedrock/play/block_pick_request.rs
Normal file
111
crates/pumpkin/src/net/bedrock/play/block_pick_request.rs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
47
crates/pumpkin/src/net/bedrock/play/chat_command.rs
Normal file
47
crates/pumpkin/src/net/bedrock/play/chat_command.rs
Normal 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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
}
|
||||||
55
crates/pumpkin/src/net/bedrock/play/chat_message.rs
Normal file
55
crates/pumpkin/src/net/bedrock/play/chat_message.rs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
}
|
||||||
49
crates/pumpkin/src/net/bedrock/play/container_close.rs
Normal file
49
crates/pumpkin/src/net/bedrock/play/container_close.rs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
34
crates/pumpkin/src/net/bedrock/play/emote.rs
Normal file
34
crates/pumpkin/src/net/bedrock/play/emote.rs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
12
crates/pumpkin/src/net/bedrock/play/emote_list.rs
Normal file
12
crates/pumpkin/src/net/bedrock/play/emote_list.rs
Normal 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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
31
crates/pumpkin/src/net/bedrock/play/interaction.rs
Normal file
31
crates/pumpkin/src/net/bedrock/play/interaction.rs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
457
crates/pumpkin/src/net/bedrock/play/inventory_action.rs
Normal file
457
crates/pumpkin/src/net/bedrock/play/inventory_action.rs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
856
crates/pumpkin/src/net/bedrock/play/item_stack_request.rs
Normal file
856
crates/pumpkin/src/net/bedrock/play/item_stack_request.rs
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
42
crates/pumpkin/src/net/bedrock/play/mob_equipment.rs
Normal file
42
crates/pumpkin/src/net/bedrock/play/mob_equipment.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
154
crates/pumpkin/src/net/bedrock/play/mod.rs
Normal file
154
crates/pumpkin/src/net/bedrock/play/mod.rs
Normal 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;
|
||||||
22
crates/pumpkin/src/net/bedrock/play/modal_form_response.rs
Normal file
22
crates/pumpkin/src/net/bedrock/play/modal_form_response.rs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
213
crates/pumpkin/src/net/bedrock/play/player_action.rs
Normal file
213
crates/pumpkin/src/net/bedrock/play/player_action.rs
Normal 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
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
208
crates/pumpkin/src/net/bedrock/play/player_auth_input.rs
Normal file
208
crates/pumpkin/src/net/bedrock/play/player_auth_input.rs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
28
crates/pumpkin/src/net/bedrock/play/player_block_action.rs
Normal file
28
crates/pumpkin/src/net/bedrock/play/player_block_action.rs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
34
crates/pumpkin/src/net/bedrock/play/request_ability.rs
Normal file
34
crates/pumpkin/src/net/bedrock/play/request_ability.rs
Normal 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}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
51
crates/pumpkin/src/net/bedrock/play/request_chunk_radius.rs
Normal file
51
crates/pumpkin/src/net/bedrock/play/request_chunk_radius.rs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
26
crates/pumpkin/src/net/bedrock/play/respawn.rs
Normal file
26
crates/pumpkin/src/net/bedrock/play/respawn.rs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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 ®istry {
|
|
||||||
if reg.registry_id == "minecraft:dimension_type" {
|
|
||||||
sent_dimension_type = true;
|
|
||||||
}
|
|
||||||
self.send_packet_now(&CRegistryData::new(®.registry_id, ®.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()
|
|
||||||
}
|
|
||||||
39
crates/pumpkin/src/net/java/config/client_information.rs
Normal file
39
crates/pumpkin/src/net/java/config/client_information.rs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
53
crates/pumpkin/src/net/java/config/config_acknowledged.rs
Normal file
53
crates/pumpkin/src/net/java/config/config_acknowledged.rs
Normal 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()
|
||||||
|
}
|
||||||
14
crates/pumpkin/src/net/java/config/cookie_response.rs
Normal file
14
crates/pumpkin/src/net/java/config/cookie_response.rs
Normal 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()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
18
crates/pumpkin/src/net/java/config/keep_alive.rs
Normal file
18
crates/pumpkin/src/net/java/config/keep_alive.rs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
86
crates/pumpkin/src/net/java/config/known_packs.rs
Normal file
86
crates/pumpkin/src/net/java/config/known_packs.rs
Normal 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 ®istry {
|
||||||
|
if reg.registry_id == "minecraft:dimension_type" {
|
||||||
|
sent_dimension_type = true;
|
||||||
|
}
|
||||||
|
self.send_packet_now(&CRegistryData::new(®.registry_id, ®.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
|
||||||
|
}
|
||||||
|
}
|
||||||
35
crates/pumpkin/src/net/java/config/mod.rs
Normal file
35
crates/pumpkin/src/net/java/config/mod.rs
Normal 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;
|
||||||
15
crates/pumpkin/src/net/java/config/plugin_message.rs
Normal file
15
crates/pumpkin/src/net/java/config/plugin_message.rs
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
84
crates/pumpkin/src/net/java/config/resource_pack.rs
Normal file
84
crates/pumpkin/src/net/java/config/resource_pack.rs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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 ®istry {
|
|
||||||
self.send_packet_now(&CRegistryData::new(®.registry_id, ®.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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
12
crates/pumpkin/src/net/java/login/cookie_response.rs
Normal file
12
crates/pumpkin/src/net/java/login/cookie_response.rs
Normal 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())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
181
crates/pumpkin/src/net/java/login/encryption_response.rs
Normal file
181
crates/pumpkin/src/net/java/login/encryption_response.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
47
crates/pumpkin/src/net/java/login/known_packs.rs
Normal file
47
crates/pumpkin/src/net/java/login/known_packs.rs
Normal 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 ®istry {
|
||||||
|
self.send_packet_now(&CRegistryData::new(®.registry_id, ®.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
112
crates/pumpkin/src/net/java/login/login_acknowledged.rs
Normal file
112
crates/pumpkin/src/net/java/login/login_acknowledged.rs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
78
crates/pumpkin/src/net/java/login/login_start.rs
Normal file
78
crates/pumpkin/src/net/java/login/login_start.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
39
crates/pumpkin/src/net/java/login/mod.rs
Normal file
39
crates/pumpkin/src/net/java/login/mod.rs
Normal 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;
|
||||||
27
crates/pumpkin/src/net/java/login/plugin_response.rs
Normal file
27
crates/pumpkin/src/net/java/login/plugin_response.rs
Normal 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
58
crates/pumpkin/src/net/java/play/attack.rs
Normal file
58
crates/pumpkin/src/net/java/play/attack.rs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
27
crates/pumpkin/src/net/java/play/bundle_item_selected.rs
Normal file
27
crates/pumpkin/src/net/java/play/bundle_item_selected.rs
Normal 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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
26
crates/pumpkin/src/net/java/play/change_game_mode.rs
Normal file
26
crates/pumpkin/src/net/java/play/change_game_mode.rs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
45
crates/pumpkin/src/net/java/play/chat_command.rs
Normal file
45
crates/pumpkin/src/net/java/play/chat_command.rs
Normal 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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
}
|
||||||
220
crates/pumpkin/src/net/java/play/chat_message.rs
Normal file
220
crates/pumpkin/src/net/java/play/chat_message.rs
Normal 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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
16
crates/pumpkin/src/net/java/play/chunk_batch.rs
Normal file
16
crates/pumpkin/src/net/java/play/chunk_batch.rs
Normal 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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
38
crates/pumpkin/src/net/java/play/client_command.rs
Normal file
38
crates/pumpkin/src/net/java/play/client_command.rs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
89
crates/pumpkin/src/net/java/play/client_information.rs
Normal file
89
crates/pumpkin/src/net/java/play/client_information.rs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
13
crates/pumpkin/src/net/java/play/close_container.rs
Normal file
13
crates/pumpkin/src/net/java/play/close_container.rs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
36
crates/pumpkin/src/net/java/play/command_suggestion.rs
Normal file
36
crates/pumpkin/src/net/java/play/command_suggestion.rs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
31
crates/pumpkin/src/net/java/play/confirm_teleport.rs
Normal file
31
crates/pumpkin/src/net/java/play/confirm_teleport.rs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
13
crates/pumpkin/src/net/java/play/cookie_response.rs
Normal file
13
crates/pumpkin/src/net/java/play/cookie_response.rs
Normal 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())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
36
crates/pumpkin/src/net/java/play/edit_book.rs
Normal file
36
crates/pumpkin/src/net/java/play/edit_book.rs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
129
crates/pumpkin/src/net/java/play/interact.rs
Normal file
129
crates/pumpkin/src/net/java/play/interact.rs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
21
crates/pumpkin/src/net/java/play/jigsaw_generate.rs
Normal file
21
crates/pumpkin/src/net/java/play/jigsaw_generate.rs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
24
crates/pumpkin/src/net/java/play/keep_alive.rs
Normal file
24
crates/pumpkin/src/net/java/play/keep_alive.rs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
265
crates/pumpkin/src/net/java/play/mod.rs
Normal file
265
crates/pumpkin/src/net/java/play/mod.rs
Normal 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;
|
||||||
18
crates/pumpkin/src/net/java/play/move_vehicle.rs
Normal file
18
crates/pumpkin/src/net/java/play/move_vehicle.rs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
13
crates/pumpkin/src/net/java/play/paddle_boat.rs
Normal file
13
crates/pumpkin/src/net/java/play/paddle_boat.rs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
120
crates/pumpkin/src/net/java/play/pick_item.rs
Normal file
120
crates/pumpkin/src/net/java/play/pick_item.rs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
9
crates/pumpkin/src/net/java/play/ping_request.rs
Normal file
9
crates/pumpkin/src/net/java/play/ping_request.rs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
213
crates/pumpkin/src/net/java/play/place_recipe.rs
Normal file
213
crates/pumpkin/src/net/java/play/place_recipe.rs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
34
crates/pumpkin/src/net/java/play/player_abilities.rs
Normal file
34
crates/pumpkin/src/net/java/play/player_abilities.rs
Normal 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;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
286
crates/pumpkin/src/net/java/play/player_action.rs
Normal file
286
crates/pumpkin/src/net/java/play/player_action.rs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
67
crates/pumpkin/src/net/java/play/player_command.rs
Normal file
67
crates/pumpkin/src/net/java/play/player_command.rs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
12
crates/pumpkin/src/net/java/play/player_ground.rs
Normal file
12
crates/pumpkin/src/net/java/play/player_ground.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
52
crates/pumpkin/src/net/java/play/player_input.rs
Normal file
52
crates/pumpkin/src/net/java/play/player_input.rs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
8
crates/pumpkin/src/net/java/play/player_loaded.rs
Normal file
8
crates/pumpkin/src/net/java/play/player_loaded.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
347
crates/pumpkin/src/net/java/play/player_position.rs
Normal file
347
crates/pumpkin/src/net/java/play/player_position.rs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
63
crates/pumpkin/src/net/java/play/player_rotation.rs
Normal file
63
crates/pumpkin/src/net/java/play/player_rotation.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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.
|
||||||
|
}
|
||||||
|
}
|
||||||
13
crates/pumpkin/src/net/java/play/recipe_book_seen_recipe.rs
Normal file
13
crates/pumpkin/src/net/java/play/recipe_book_seen_recipe.rs
Normal 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.
|
||||||
|
}
|
||||||
|
}
|
||||||
18
crates/pumpkin/src/net/java/play/seen_advancement.rs
Normal file
18
crates/pumpkin/src/net/java/play/seen_advancement.rs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
17
crates/pumpkin/src/net/java/play/select_trade.rs
Normal file
17
crates/pumpkin/src/net/java/play/select_trade.rs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
97
crates/pumpkin/src/net/java/play/set_command_block.rs
Normal file
97
crates/pumpkin/src/net/java/play/set_command_block.rs
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
68
crates/pumpkin/src/net/java/play/set_creative_slot.rs
Normal file
68
crates/pumpkin/src/net/java/play/set_creative_slot.rs
Normal 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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
39
crates/pumpkin/src/net/java/play/set_held_item.rs
Normal file
39
crates/pumpkin/src/net/java/play/set_held_item.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
40
crates/pumpkin/src/net/java/play/set_jigsaw_block.rs
Normal file
40
crates/pumpkin/src/net/java/play/set_jigsaw_block.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
15
crates/pumpkin/src/net/java/play/set_test_block.rs
Normal file
15
crates/pumpkin/src/net/java/play/set_test_block.rs
Normal 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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
63
crates/pumpkin/src/net/java/play/swing_arm.rs
Normal file
63
crates/pumpkin/src/net/java/play/swing_arm.rs
Normal 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;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
}
|
||||||
37
crates/pumpkin/src/net/java/play/teleport_to_entity.rs
Normal file
37
crates/pumpkin/src/net/java/play/teleport_to_entity.rs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
35
crates/pumpkin/src/net/java/play/update_sign.rs
Normal file
35
crates/pumpkin/src/net/java/play/update_sign.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
163
crates/pumpkin/src/net/java/play/use_item.rs
Normal file
163
crates/pumpkin/src/net/java/play/use_item.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
262
crates/pumpkin/src/net/java/play/use_item_on.rs
Normal file
262
crates/pumpkin/src/net/java/play/use_item_on.rs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user