- We moved all players to seperate worlds. This will save performance when many different players are on different worlds. The only thing broken now are commands because of async stuff
537 lines
21 KiB
Rust
537 lines
21 KiB
Rust
use std::f32::consts::PI;
|
|
|
|
use crate::{
|
|
commands::{handle_command, CommandSender},
|
|
entity::player::{ChatMode, GameMode, Hand, Player},
|
|
server::Server,
|
|
util::math::wrap_degrees,
|
|
};
|
|
use num_traits::FromPrimitive;
|
|
use pumpkin_core::text::TextComponent;
|
|
use pumpkin_entity::EntityId;
|
|
use pumpkin_inventory::WindowType;
|
|
use pumpkin_protocol::server::play::{SCloseContainer, SSetPlayerGround, SUseItem};
|
|
use pumpkin_protocol::{
|
|
client::play::{
|
|
Animation, CAcknowledgeBlockChange, CBlockUpdate, CEntityAnimation, CEntityVelocity,
|
|
CHeadRot, CHurtAnimation, CPingResponse, CPlayerChatMessage, CUpdateEntityPos,
|
|
CUpdateEntityPosRot, CUpdateEntityRot, CWorldEvent, FilterType,
|
|
},
|
|
position::WorldPosition,
|
|
server::play::{
|
|
Action, ActionType, SChatCommand, SChatMessage, SClientInformationPlay, SConfirmTeleport,
|
|
SInteract, SPlayPingRequest, SPlayerAction, SPlayerCommand, SPlayerPosition,
|
|
SPlayerPositionRotation, SPlayerRotation, SSetCreativeSlot, SSetHeldItem, SSwingArm,
|
|
SUseItemOn, Status,
|
|
},
|
|
};
|
|
use pumpkin_world::block::{BlockFace, BlockId};
|
|
use pumpkin_world::global_registry;
|
|
|
|
use super::PlayerConfig;
|
|
|
|
fn modulus(a: f32, b: f32) -> f32 {
|
|
((a % b) + b) % b
|
|
}
|
|
|
|
/// Handles all Play Packets send by a real Player
|
|
/// NEVER TRUST THE CLIENT. HANDLE EVERY ERROR, UNWRAP/EXPECT ARE FORBIDDEN
|
|
impl Player {
|
|
pub fn handle_confirm_teleport(
|
|
&mut self,
|
|
_server: &mut Server,
|
|
confirm_teleport: SConfirmTeleport,
|
|
) {
|
|
if let Some((id, position)) = self.awaiting_teleport.as_ref() {
|
|
if id == &confirm_teleport.teleport_id {
|
|
// we should set the pos now to that we requested in the teleport packet, Is may fixed issues when the client sended position packets while being teleported
|
|
self.entity.x = position.x;
|
|
self.entity.y = position.y;
|
|
self.entity.z = position.z;
|
|
|
|
self.awaiting_teleport = None;
|
|
} else {
|
|
self.kick(TextComponent::text("Wrong teleport id"))
|
|
}
|
|
} else {
|
|
self.kick(TextComponent::text(
|
|
"Send Teleport confirm, but we did not teleport",
|
|
))
|
|
}
|
|
}
|
|
|
|
fn clamp_horizontal(pos: f64) -> f64 {
|
|
pos.clamp(-3.0E7, 3.0E7)
|
|
}
|
|
|
|
fn clamp_vertical(pos: f64) -> f64 {
|
|
pos.clamp(-2.0E7, 2.0E7)
|
|
}
|
|
|
|
pub async fn handle_position(&mut self, _server: &mut Server, position: SPlayerPosition) {
|
|
if position.x.is_nan() || position.feet_y.is_nan() || position.z.is_nan() {
|
|
self.kick(TextComponent::text("Invalid movement"));
|
|
return;
|
|
}
|
|
let entity = &mut self.entity;
|
|
entity.lastx = entity.x;
|
|
entity.lasty = entity.y;
|
|
entity.lastz = entity.z;
|
|
entity.x = Self::clamp_horizontal(position.x);
|
|
entity.y = Self::clamp_vertical(position.feet_y);
|
|
entity.z = Self::clamp_horizontal(position.z);
|
|
// TODO: teleport when moving > 8 block
|
|
|
|
// send new position to all other players
|
|
let on_ground = self.on_ground;
|
|
let entity_id = entity.entity_id;
|
|
let (x, lastx) = (entity.x, entity.lastx);
|
|
let (y, lasty) = (entity.y, entity.lasty);
|
|
let (z, lastz) = (entity.z, entity.lastz);
|
|
let world = self.world.lock().await;
|
|
world.broadcast_packet(
|
|
&[&self.client.token],
|
|
&CUpdateEntityPos::new(
|
|
entity_id.into(),
|
|
(x * 4096.0 - lastx * 4096.0) as i16,
|
|
(y * 4096.0 - lasty * 4096.0) as i16,
|
|
(z * 4096.0 - lastz * 4096.0) as i16,
|
|
on_ground,
|
|
),
|
|
);
|
|
}
|
|
|
|
pub async fn handle_position_rotation(
|
|
&mut self,
|
|
_server: &mut Server,
|
|
position_rotation: SPlayerPositionRotation,
|
|
) {
|
|
if position_rotation.x.is_nan()
|
|
|| position_rotation.feet_y.is_nan()
|
|
|| position_rotation.z.is_nan()
|
|
{
|
|
self.kick(TextComponent::text("Invalid movement"));
|
|
return;
|
|
}
|
|
if !position_rotation.yaw.is_finite() || !position_rotation.pitch.is_finite() {
|
|
self.kick(TextComponent::text("Invalid rotation"));
|
|
return;
|
|
}
|
|
let entity = &mut self.entity;
|
|
|
|
entity.lastx = entity.x;
|
|
entity.lasty = entity.y;
|
|
entity.lastz = entity.z;
|
|
entity.x = Self::clamp_horizontal(position_rotation.x);
|
|
entity.y = Self::clamp_vertical(position_rotation.feet_y);
|
|
entity.z = Self::clamp_horizontal(position_rotation.z);
|
|
entity.yaw = wrap_degrees(position_rotation.yaw) % 360.0;
|
|
entity.pitch = wrap_degrees(position_rotation.pitch).clamp(-90.0, 90.0) % 360.0;
|
|
|
|
// send new position to all other players
|
|
let on_ground = self.on_ground;
|
|
let entity_id = entity.entity_id;
|
|
let (x, lastx) = (entity.x, entity.lastx);
|
|
let (y, lasty) = (entity.y, entity.lasty);
|
|
let (z, lastz) = (entity.z, entity.lastz);
|
|
let yaw = modulus(entity.yaw * 256.0 / 360.0, 256.0);
|
|
let pitch = modulus(entity.pitch * 256.0 / 360.0, 256.0);
|
|
// let head_yaw = (entity.head_yaw * 256.0 / 360.0).floor();
|
|
let world = self.world.lock().await;
|
|
|
|
world.broadcast_packet(
|
|
&[&self.client.token],
|
|
&CUpdateEntityPosRot::new(
|
|
entity_id.into(),
|
|
(x * 4096.0 - lastx * 4096.0) as i16,
|
|
(y * 4096.0 - lasty * 4096.0) as i16,
|
|
(z * 4096.0 - lastz * 4096.0) as i16,
|
|
yaw as u8,
|
|
pitch as u8,
|
|
on_ground,
|
|
),
|
|
);
|
|
world.broadcast_packet(
|
|
&[&self.client.token],
|
|
&CHeadRot::new(entity_id.into(), yaw as u8),
|
|
);
|
|
}
|
|
|
|
pub async fn handle_rotation(&mut self, _server: &mut Server, rotation: SPlayerRotation) {
|
|
if !rotation.yaw.is_finite() || !rotation.pitch.is_finite() {
|
|
self.kick(TextComponent::text("Invalid rotation"));
|
|
return;
|
|
}
|
|
let entity = &mut self.entity;
|
|
entity.yaw = wrap_degrees(rotation.yaw) % 360.0;
|
|
entity.pitch = wrap_degrees(rotation.pitch).clamp(-90.0, 90.0) % 360.0;
|
|
// send new position to all other players
|
|
let on_ground = self.on_ground;
|
|
let entity_id = entity.entity_id;
|
|
let yaw = modulus(entity.yaw * 256.0 / 360.0, 256.0);
|
|
let pitch = modulus(entity.pitch * 256.0 / 360.0, 256.0);
|
|
// let head_yaw = modulus(entity.head_yaw * 256.0 / 360.0, 256.0);
|
|
|
|
let world = self.world.lock().await;
|
|
let packet = CUpdateEntityRot::new(entity_id.into(), yaw as u8, pitch as u8, on_ground);
|
|
// self.client.send_packet(&packet);
|
|
world.broadcast_packet(&[&self.client.token], &packet);
|
|
let packet = CHeadRot::new(entity_id.into(), yaw as u8);
|
|
// self.client.send_packet(&packet);
|
|
world.broadcast_packet(&[&self.client.token], &packet);
|
|
}
|
|
|
|
pub fn handle_chat_command(&mut self, server: &mut Server, command: SChatCommand) {
|
|
handle_command(&mut CommandSender::Player(self), server, &command.command);
|
|
}
|
|
|
|
pub fn handle_player_ground(&mut self, _server: &mut Server, ground: SSetPlayerGround) {
|
|
self.on_ground = ground.on_ground;
|
|
}
|
|
|
|
pub async fn handle_player_command(&mut self, _server: &mut Server, command: SPlayerCommand) {
|
|
if command.entity_id != self.entity.entity_id.into() {
|
|
return;
|
|
}
|
|
|
|
if let Some(action) = Action::from_i32(command.action.0) {
|
|
match action {
|
|
pumpkin_protocol::server::play::Action::StartSneaking => {
|
|
if !self.sneaking {
|
|
self.set_sneaking(true).await
|
|
}
|
|
}
|
|
pumpkin_protocol::server::play::Action::StopSneaking => {
|
|
if self.sneaking {
|
|
self.set_sneaking(false).await
|
|
}
|
|
}
|
|
pumpkin_protocol::server::play::Action::LeaveBed => todo!(),
|
|
pumpkin_protocol::server::play::Action::StartSprinting => {
|
|
if !self.sprinting {
|
|
self.set_sprinting(true).await
|
|
}
|
|
}
|
|
pumpkin_protocol::server::play::Action::StopSprinting => {
|
|
if self.sprinting {
|
|
self.set_sprinting(false).await
|
|
}
|
|
}
|
|
pumpkin_protocol::server::play::Action::StartHorseJump => todo!(),
|
|
pumpkin_protocol::server::play::Action::StopHorseJump => todo!(),
|
|
pumpkin_protocol::server::play::Action::OpenVehicleInventory => todo!(),
|
|
pumpkin_protocol::server::play::Action::StartFlyingElytra => {} // TODO
|
|
}
|
|
} else {
|
|
self.kick(TextComponent::text("Invalid player command"))
|
|
}
|
|
}
|
|
|
|
pub async fn handle_swing_arm(&mut self, _server: &mut Server, swing_arm: SSwingArm) {
|
|
match Hand::from_i32(swing_arm.hand.0) {
|
|
Some(hand) => {
|
|
let animation = match hand {
|
|
Hand::Main => Animation::SwingMainArm,
|
|
Hand::Off => Animation::SwingOffhand,
|
|
};
|
|
let id = self.entity_id();
|
|
let world = self.world.lock().await;
|
|
world.broadcast_packet(
|
|
&[&self.client.token],
|
|
&CEntityAnimation::new(id.into(), animation as u8),
|
|
)
|
|
}
|
|
None => {
|
|
self.kick(TextComponent::text("Invalid hand"));
|
|
}
|
|
};
|
|
}
|
|
|
|
pub async fn handle_chat_message(&mut self, _server: &mut Server, chat_message: SChatMessage) {
|
|
dbg!("got message");
|
|
|
|
let message = chat_message.message;
|
|
if message.len() > 256 {
|
|
self.kick(TextComponent::text("Oversized message"));
|
|
return;
|
|
}
|
|
|
|
// TODO: filter message & validation
|
|
let gameprofile = &self.gameprofile;
|
|
|
|
let world = self.world.lock().await;
|
|
world.broadcast_packet(
|
|
&[&self.client.token],
|
|
&CPlayerChatMessage::new(
|
|
pumpkin_protocol::uuid::UUID(gameprofile.id),
|
|
1.into(),
|
|
chat_message.signature.as_deref(),
|
|
&message,
|
|
chat_message.timestamp,
|
|
chat_message.salt,
|
|
&[],
|
|
Some(TextComponent::text(&message)),
|
|
FilterType::PassThrough,
|
|
1.into(),
|
|
TextComponent::text(&gameprofile.name.clone()),
|
|
None,
|
|
),
|
|
)
|
|
|
|
/* server.broadcast_packet(
|
|
self,
|
|
&CDisguisedChatMessage::new(
|
|
TextComponent::from(message.clone()),
|
|
VarInt(0),
|
|
gameprofile.name.clone().into(),
|
|
None,
|
|
),
|
|
) */
|
|
}
|
|
|
|
pub fn handle_client_information_play(
|
|
&mut self,
|
|
_server: &mut Server,
|
|
client_information: SClientInformationPlay,
|
|
) {
|
|
if let (Some(main_hand), Some(chat_mode)) = (
|
|
Hand::from_i32(client_information.main_hand.into()),
|
|
ChatMode::from_i32(client_information.chat_mode.into()),
|
|
) {
|
|
self.client.config = Some(PlayerConfig {
|
|
locale: client_information.locale,
|
|
view_distance: client_information.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,
|
|
});
|
|
} else {
|
|
self.kick(TextComponent::text("Invalid hand or chat type"))
|
|
}
|
|
}
|
|
|
|
pub async fn handle_interact(&mut self, server: &mut Server, interact: SInteract) {
|
|
let sneaking = interact.sneaking;
|
|
if self.sneaking != sneaking {
|
|
self.set_sneaking(sneaking).await;
|
|
}
|
|
match ActionType::from_i32(interact.typ.0) {
|
|
Some(action) => match action {
|
|
ActionType::Attack => {
|
|
let entity_id = interact.entity_id;
|
|
// TODO: do validation and stuff
|
|
let config = &server.advanced_config.pvp;
|
|
if config.enabled {
|
|
let world = self.world.clone();
|
|
let world = world.lock().await;
|
|
let attacked_player = world.get_by_entityid(self, entity_id.0 as EntityId);
|
|
if let Some(mut player) = attacked_player {
|
|
let token = player.client.token.clone();
|
|
let velo = player.velocity;
|
|
if config.protect_creative && player.gamemode == GameMode::Creative {
|
|
return;
|
|
}
|
|
if config.knockback {
|
|
let yaw = self.entity.yaw;
|
|
let strength = 1.0;
|
|
player.knockback(
|
|
strength * 0.5,
|
|
(yaw * (PI / 180.0)).sin() as f64,
|
|
-(yaw * (PI / 180.0)).cos() as f64,
|
|
);
|
|
let packet = &CEntityVelocity::new(
|
|
&entity_id,
|
|
player.velocity.x as f32,
|
|
player.velocity.y as f32,
|
|
player.velocity.z as f32,
|
|
);
|
|
self.velocity = self.velocity.multiply(0.6, 1.0, 0.6);
|
|
|
|
player.velocity = velo;
|
|
player.client.send_packet(packet);
|
|
}
|
|
if config.hurt_animation {
|
|
// TODO
|
|
// thats how we prevent borrow errors :c
|
|
let packet = &CHurtAnimation::new(&entity_id, self.entity.yaw);
|
|
self.client.send_packet(packet);
|
|
player.client.send_packet(packet);
|
|
world.broadcast_packet(
|
|
&[&self.client.token, &token],
|
|
&CHurtAnimation::new(&entity_id, 10.0),
|
|
)
|
|
}
|
|
if config.swing {}
|
|
} else {
|
|
self.kick(TextComponent::text("Interacted with invalid entity id"))
|
|
}
|
|
}
|
|
}
|
|
ActionType::Interact => {
|
|
dbg!("todo");
|
|
}
|
|
ActionType::InteractAt => {
|
|
dbg!("todo");
|
|
}
|
|
},
|
|
None => self.kick(TextComponent::text("Invalid action type")),
|
|
}
|
|
}
|
|
pub async fn handle_player_action(
|
|
&mut self,
|
|
_server: &mut Server,
|
|
player_action: SPlayerAction,
|
|
) {
|
|
match Status::from_i32(player_action.status.0) {
|
|
Some(status) => match status {
|
|
Status::StartedDigging => {
|
|
if !self.can_interact_with_block_at(&player_action.location, 1.0) {
|
|
// TODO: maybe log?
|
|
return;
|
|
}
|
|
// TODO: do validation
|
|
// TODO: Config
|
|
if self.gamemode == GameMode::Creative {
|
|
let location = player_action.location;
|
|
// Block break & block break sound
|
|
// TODO: currently this is always dirt replace it
|
|
let world = self.world.lock().await;
|
|
world.broadcast_packet(
|
|
&[&self.client.token],
|
|
&CWorldEvent::new(2001, &location, 11, false),
|
|
);
|
|
// AIR
|
|
world.broadcast_packet(
|
|
&[&self.client.token],
|
|
&CBlockUpdate::new(&location, 0.into()),
|
|
);
|
|
}
|
|
}
|
|
Status::CancelledDigging => {
|
|
if !self.can_interact_with_block_at(&player_action.location, 1.0) {
|
|
// TODO: maybe log?
|
|
return;
|
|
}
|
|
self.current_block_destroy_stage = 0;
|
|
}
|
|
Status::FinishedDigging => {
|
|
// TODO: do validation
|
|
let location = player_action.location;
|
|
if !self.can_interact_with_block_at(&location, 1.0) {
|
|
// TODO: maybe log?
|
|
return;
|
|
}
|
|
// Block break & block break sound
|
|
// TODO: currently this is always dirt replace it
|
|
let world = self.world.lock().await;
|
|
world.broadcast_packet(
|
|
&[&self.client.token],
|
|
&CWorldEvent::new(2001, &location, 11, false),
|
|
);
|
|
// AIR
|
|
world.broadcast_packet(
|
|
&[&self.client.token],
|
|
&CBlockUpdate::new(&location, 0.into()),
|
|
);
|
|
// TODO: Send this every tick
|
|
self.client
|
|
.send_packet(&CAcknowledgeBlockChange::new(player_action.sequence));
|
|
}
|
|
Status::DropItemStack => {
|
|
dbg!("todo");
|
|
}
|
|
Status::DropItem => {
|
|
dbg!("todo");
|
|
}
|
|
Status::ShootArrowOrFinishEating => {
|
|
dbg!("todo");
|
|
}
|
|
Status::SwapItem => {
|
|
dbg!("todo");
|
|
}
|
|
},
|
|
None => self.kick(TextComponent::text("Invalid status")),
|
|
}
|
|
}
|
|
|
|
pub fn handle_play_ping_request(&mut self, _server: &mut Server, request: SPlayPingRequest) {
|
|
self.client
|
|
.send_packet(&CPingResponse::new(request.payload));
|
|
}
|
|
|
|
pub async fn handle_use_item_on(&mut self, _server: &mut Server, use_item_on: SUseItemOn) {
|
|
let location = use_item_on.location;
|
|
|
|
if !self.can_interact_with_block_at(&location, 1.0) {
|
|
// TODO: maybe log?
|
|
return;
|
|
}
|
|
|
|
if let Some(face) = BlockFace::from_i32(use_item_on.face.0) {
|
|
if let Some(item) = self.inventory.held_item() {
|
|
let minecraft_id = global_registry::find_minecraft_id(
|
|
global_registry::ITEM_REGISTRY,
|
|
item.item_id,
|
|
)
|
|
.expect("All item ids are in the global registry");
|
|
if let Ok(block_state_id) = BlockId::new(minecraft_id, None) {
|
|
let world = self.world.lock().await;
|
|
world.broadcast_packet(
|
|
&[&self.client.token],
|
|
&CBlockUpdate::new(&location, block_state_id.get_id_mojang_repr().into()),
|
|
);
|
|
world.broadcast_packet(
|
|
&[&self.client.token],
|
|
&CBlockUpdate::new(
|
|
&WorldPosition(location.0 + face.to_offset()),
|
|
block_state_id.get_id_mojang_repr().into(),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
self.client
|
|
.send_packet(&CAcknowledgeBlockChange::new(use_item_on.sequence));
|
|
} else {
|
|
self.kick(TextComponent::text("Invalid block face"))
|
|
}
|
|
}
|
|
|
|
pub fn handle_use_item(&mut self, _server: &mut Server, _use_item: SUseItem) {
|
|
// TODO: handle packet correctly
|
|
log::error!("An item was used(SUseItem), but the packet is not implemented yet");
|
|
}
|
|
|
|
pub fn handle_set_held_item(&mut self, _server: &mut Server, held: SSetHeldItem) {
|
|
let slot = held.slot;
|
|
if !(0..=8).contains(&slot) {
|
|
self.kick(TextComponent::text("Invalid held slot"))
|
|
}
|
|
self.inventory.set_selected(slot as usize);
|
|
}
|
|
|
|
pub fn handle_set_creative_slot(&mut self, _server: &mut Server, packet: SSetCreativeSlot) {
|
|
if self.gamemode != GameMode::Creative {
|
|
self.kick(TextComponent::text(
|
|
"Invalid action, you can only do that if you are in creative",
|
|
));
|
|
return;
|
|
}
|
|
self.inventory
|
|
.set_slot(packet.slot as usize, packet.clicked_item.to_item(), false);
|
|
}
|
|
|
|
// TODO:
|
|
// This function will in the future be used to keep track of if the client is in a valid state.
|
|
// But this is not possible yet
|
|
pub fn handle_close_container(&mut self, _server: &mut Server, packet: SCloseContainer) {
|
|
// window_id 0 represents both 9x1 Generic AND inventory here
|
|
let Some(_window_type) = WindowType::from_u8(packet.window_id) else {
|
|
self.kick(TextComponent::text("Invalid window ID"));
|
|
return;
|
|
};
|
|
}
|
|
}
|