429 lines
15 KiB
Rust
429 lines
15 KiB
Rust
use std::{
|
|
num::{NonZero, NonZeroI32},
|
|
sync::{Arc, atomic::Ordering},
|
|
};
|
|
|
|
use pumpkin_macros::send_cancellable;
|
|
use pumpkin_protocol::{
|
|
bedrock::{
|
|
client::{chunk_radius_update::CChunkRadiusUpdate, container_open::CContainerOpen},
|
|
server::{
|
|
animate::{AnimateAction, SAnimate},
|
|
command_request::SCommandRequest,
|
|
container_close::SContainerClose,
|
|
interaction::{Action, SInteraction},
|
|
player_action::{Action as PlayerAction, SPlayerAction},
|
|
player_auth_input::{InputData, SPlayerAuthInput},
|
|
request_chunk_radius::SRequestChunkRadius,
|
|
set_local_player_as_initialized::SSetLocalPlayerAsInitialized,
|
|
text::SText,
|
|
},
|
|
},
|
|
codec::{var_int::VarInt, var_long::VarLong},
|
|
java::client::play::{Animation, CSystemChatMessage},
|
|
};
|
|
use pumpkin_util::{GameMode, math::position::BlockPos, text::TextComponent};
|
|
|
|
use pumpkin_world::world::BlockFlags;
|
|
|
|
use crate::{
|
|
entity::{EntityBase, player::Player},
|
|
net::{DisconnectReason, bedrock::BedrockClient},
|
|
plugin::player::{
|
|
player_chat::PlayerChatEvent, player_command_send::PlayerCommandSendEvent,
|
|
player_toggle_flight_event::PlayerToggleFlightEvent,
|
|
},
|
|
server::{Server, seasonal_events},
|
|
world::chunker::{self},
|
|
};
|
|
use tracing::{debug, info};
|
|
|
|
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 server = player.world().server.upgrade().unwrap();
|
|
|
|
let view_distance =
|
|
chunk_radius.clamp(2, NonZeroI32::from(server.basic_config.view_distance).get());
|
|
|
|
self.send_game_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).expect("View distance must be > 0");
|
|
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;
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
pub async fn handle_player_auth_input(
|
|
&self,
|
|
player: &Arc<Player>,
|
|
packet: SPlayerAuthInput,
|
|
server: &Server,
|
|
) {
|
|
if !player.has_client_loaded() {
|
|
return;
|
|
}
|
|
let new_pos = packet.position.to_f64();
|
|
let old_pos = player.position();
|
|
|
|
if new_pos != old_pos {
|
|
player.living_entity.entity.set_pos(new_pos);
|
|
chunker::update_position(player).await;
|
|
}
|
|
|
|
let input_data = packet.input_data;
|
|
let entity = player.get_entity();
|
|
|
|
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 mut abilities = player.abilities.lock().await;
|
|
if !abilities.flying {
|
|
send_cancellable! {{
|
|
server;
|
|
PlayerToggleFlightEvent::new(player.clone(), true);
|
|
'after: {
|
|
abilities.flying = true;
|
|
player.send_abilities_update().await;
|
|
}
|
|
'cancelled: {
|
|
player.send_abilities_update().await;
|
|
}
|
|
}}
|
|
}
|
|
} else if input_data.get(InputData::StopFlying as usize) {
|
|
let mut abilities = player.abilities.lock().await;
|
|
if abilities.flying {
|
|
send_cancellable! {{
|
|
server;
|
|
PlayerToggleFlightEvent::new(player.clone(), false);
|
|
'after: {
|
|
abilities.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;
|
|
}
|
|
}
|
|
}
|
|
|
|
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 action = PlayerAction::try_from(packet.action.0).unwrap();
|
|
self.handle_player_action(
|
|
player,
|
|
server,
|
|
SPlayerAction {
|
|
runtime_id: VarInt(0), // Unused
|
|
action,
|
|
block_pos: packet.block_pos,
|
|
result_pos: BlockPos::ZERO,
|
|
face: packet.face,
|
|
},
|
|
)
|
|
.await;
|
|
}
|
|
|
|
pub fn handle_animate(&self, player: &Arc<Player>, _server: &Server, packet: &SAnimate) {
|
|
if !player.has_client_loaded() {
|
|
return;
|
|
}
|
|
|
|
let entity = &player.living_entity.entity;
|
|
let _world = entity.world.load();
|
|
|
|
// Broadcast the animation to other players
|
|
let _java_animation = match packet.action {
|
|
AnimateAction::SwingArm => Some(Animation::SwingMainArm),
|
|
AnimateAction::WakeUp => Some(Animation::LeaveBed),
|
|
AnimateAction::CriticalHit => Some(Animation::CriticalEffect),
|
|
AnimateAction::MagicCriticalHit => Some(Animation::MagicCriticaleffect),
|
|
_ => None,
|
|
};
|
|
|
|
// 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),
|
|
// boat_rowing_time: packet.boat_rowing_time,
|
|
// };
|
|
// world.broadcast_editioned(&je_packet, &be_packet).await;
|
|
// }
|
|
}
|
|
|
|
pub async fn handle_interaction(&self, _player: &Arc<Player>, packet: SInteraction) {
|
|
if matches!(packet.action, Action::OpenInventory) {
|
|
self.send_game_packet(&CContainerOpen {
|
|
container_id: 0,
|
|
container_type: 0xff,
|
|
position: BlockPos::ZERO,
|
|
target_entity_id: VarLong(-1),
|
|
})
|
|
.await;
|
|
}
|
|
}
|
|
|
|
pub async fn handle_container_close(&self, _player: &Arc<Player>, packet: SContainerClose) {
|
|
if packet.container_id == 0 {
|
|
self.send_game_packet(&SContainerClose {
|
|
container_id: 0,
|
|
container_type: 0xff,
|
|
server_initiated: false,
|
|
})
|
|
.await;
|
|
}
|
|
}
|
|
|
|
pub async fn handle_chat_message(&self, server: &Server, player: &Arc<Player>, packet: SText) {
|
|
let gameprofile = &player.gameprofile;
|
|
|
|
send_cancellable! {{
|
|
server;
|
|
PlayerChatEvent::new(player.clone(), packet.message, 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.living_entity.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::new(
|
|
message, gameprofile.name.clone()
|
|
);
|
|
|
|
entity.world.load().broadcast_editioned(&je_packet, &be_packet).await;
|
|
}
|
|
}
|
|
}}
|
|
}
|
|
|
|
#[expect(clippy::match_same_arms)]
|
|
pub async fn handle_player_action(
|
|
&self,
|
|
player: &Arc<Player>,
|
|
server: &Server,
|
|
packet: SPlayerAction,
|
|
) {
|
|
if !player.has_client_loaded() {
|
|
return;
|
|
}
|
|
player.update_last_action_time();
|
|
|
|
match packet.action {
|
|
PlayerAction::StartBreak | PlayerAction::CreativePlayerDestroyBlock => {
|
|
let location = packet.block_pos;
|
|
if !player.can_interact_with_block_at(&location, 1.0) {
|
|
return;
|
|
}
|
|
|
|
let entity = &player.living_entity.entity;
|
|
let world = entity.world.load_full();
|
|
let (block, state) = world.get_block_and_state(&location);
|
|
|
|
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() {
|
|
// Broadcast that breaking started
|
|
world.set_block_breaking(entity, location, 0).await;
|
|
|
|
let speed = crate::block::calc_block_breaking(player, state, block).await;
|
|
if speed >= 1.0 {
|
|
let broken_state = world.get_block_state(&location);
|
|
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;
|
|
}
|
|
} else {
|
|
player.mining.store(true, Ordering::Relaxed);
|
|
*player.mining_pos.lock().await = location;
|
|
let progress = (speed * 10.0) as i32;
|
|
world.set_block_breaking(entity, location, progress).await;
|
|
player
|
|
.current_block_destroy_stage
|
|
.store(progress, Ordering::Relaxed);
|
|
}
|
|
}
|
|
}
|
|
PlayerAction::CrackBreak => {
|
|
// Don't do anything for this action. It is no longer used. Block
|
|
// cracking is done fully server-side.
|
|
}
|
|
PlayerAction::AbortBreak | PlayerAction::StopBreak => {
|
|
let location = packet.block_pos;
|
|
let entity = &player.living_entity.entity;
|
|
let world = entity.world.load();
|
|
|
|
player.mining.store(false, Ordering::Relaxed);
|
|
world.set_block_breaking(entity, location, -1).await;
|
|
}
|
|
// TODO
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
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
|
|
);
|
|
}
|
|
}
|
|
}}
|
|
}
|
|
|
|
pub async fn handle_modal_form_response(
|
|
&self,
|
|
player: &Arc<Player>,
|
|
server: &Server,
|
|
packet: pumpkin_protocol::bedrock::server::modal_form_response::SModalFormResponse,
|
|
) {
|
|
let event = crate::plugin::api::events::player::bedrock_form_response::BedrockFormResponseEvent::new(
|
|
player.clone(),
|
|
packet.form_id.0 as u32,
|
|
packet.form_data,
|
|
);
|
|
let _ = server.plugin_manager.fire(event).await;
|
|
}
|
|
}
|