use core::f32; use std::collections::{BinaryHeap, HashMap, HashSet, VecDeque}; use std::f64::consts::TAU; use std::mem; use std::num::NonZeroU8; use std::sync::atomic::{AtomicBool, AtomicI32, AtomicI64, AtomicU8, AtomicU32, Ordering}; use std::sync::{Arc, Weak}; use std::time::{Duration, Instant}; use arc_swap::ArcSwap; use crossbeam::atomic::AtomicCell; use crossbeam::channel::Receiver; use pumpkin_data::dimension::Dimension; use pumpkin_data::meta_data_type::MetaDataType; use pumpkin_data::tracked_data::TrackedData; use pumpkin_inventory::player::ender_chest_inventory::EnderChestInventory; use pumpkin_protocol::bedrock::client::level_chunk::CLevelChunk; use pumpkin_protocol::bedrock::client::set_time::CSetTime; use pumpkin_protocol::bedrock::client::update_abilities::{ Ability, AbilityLayer, CUpdateAbilities, }; use pumpkin_protocol::bedrock::server::text::SText; use pumpkin_protocol::codec::item_stack_seralizer::ItemStackSerializer; use pumpkin_world::chunk::{ChunkData, ChunkEntityData}; use pumpkin_world::inventory::Inventory; use tokio::sync::Mutex; use tokio::task::JoinHandle; use tracing::{debug, warn}; use uuid::Uuid; use pumpkin_data::attributes::Attributes; use pumpkin_data::block_properties::{BlockProperties, HorizontalFacing}; use pumpkin_data::damage::DamageType; use pumpkin_data::data_component_impl::{AttributeModifiersImpl, Operation}; use pumpkin_data::data_component_impl::{EquipmentSlot, EquippableImpl, ToolImpl, WeaponImpl}; use pumpkin_data::effect::StatusEffect; use pumpkin_data::entity::{EntityPose, EntityStatus, EntityType}; use pumpkin_data::item_stack::ItemStack; use pumpkin_data::particle::Particle; use pumpkin_data::sound::{Sound, SoundCategory}; use pumpkin_data::tag::Taggable; use pumpkin_data::{Block, BlockState, Enchantment, tag, translation}; use pumpkin_inventory::player::{ player_inventory::PlayerInventory, player_screen_handler::PlayerScreenHandler, }; use pumpkin_inventory::screen_handler::{ ClickType, InventoryPlayer, PlayerFuture, ScreenHandler, ScreenHandlerFactory, ScreenHandlerListener, }; use pumpkin_inventory::sync_handler::SyncHandler; use pumpkin_macros::send_cancellable; use pumpkin_nbt::pnbt::PNbtCompound; use pumpkin_protocol::IdOr; use pumpkin_protocol::SoundEvent; use pumpkin_protocol::codec::var_int::VarInt; use pumpkin_protocol::java::client::play::{ Animation, CAcknowledgeBlockChange, CActionBar, CChangeDifficulty, CChunkBatchEnd, CChunkBatchStart, CChunkData, CCloseContainer, CCombatDeath, CCustomPayload, CDisguisedChatMessage, CEntityAnimation, CEntityPositionSync, CGameEvent, CKeepAlive, COpenScreen, CParticle, CPlayerAbilities, CPlayerInfoUpdate, CPlayerPosition, CPlayerSpawnPosition, CRespawn, CSetContainerContent, CSetContainerProperty, CSetContainerSlot, CSetCursorItem, CSetEquipment, CSetExperience, CSetHealth, CSetPlayerInventory, CSetSelectedSlot, CSoundEffect, CStopSound, CSubtitle, CSystemChatMessage, CTabList, CTitleAnimation, CTitleText, CUnloadChunk, CUpdateMobEffect, CUpdateTime, GameEvent, Metadata, PlayerAction, PlayerInfoFlags, PreviousMessage, }; use pumpkin_protocol::java::server::play::{SClickSlot, SlotActionType}; use pumpkin_util::math::{ boundingbox::BoundingBox, experience, position::BlockPos, vector2::Vector2, vector3::Vector3, }; use pumpkin_util::permission::PermissionLvl; use pumpkin_util::resource_location::ResourceLocation; use pumpkin_util::text::TextComponent; use pumpkin_util::text::click::ClickEvent; use pumpkin_util::text::hover::HoverEvent; use pumpkin_util::{GameMode, Hand}; use pumpkin_world::biome; use pumpkin_world::cylindrical_chunk_iterator::Cylindrical; use pumpkin_world::level::{Level, SyncChunk, SyncEntityChunk}; use crate::block; use crate::block::blocks::bed::BedBlock; use crate::command::context::command_source::CommandSource; use crate::command::node::dispatcher::CommandDispatcher; use crate::command::{CommandSender, client_suggestions}; use crate::data::SaveJSONConfiguration; use crate::entity::{EntityBaseFuture, NbtFuture, TeleportFuture}; use crate::net::{ClientPlatform, GameProfile}; use crate::net::{DisconnectReason, PlayerConfig}; use crate::plugin::player::exp_change::PlayerExpChangeEvent; use crate::plugin::player::inventory_interact::InventoryClickEvent; use crate::plugin::player::player_change_world::PlayerChangeWorldEvent; use crate::plugin::player::player_gamemode_change::PlayerGamemodeChangeEvent; use crate::plugin::player::player_permission_check::PlayerPermissionCheckEvent; use crate::plugin::player::player_teleport::PlayerTeleportEvent; use crate::server::Server; use crate::world::World; use super::breath::BreathManager; use super::combat::{self, AttackType, player_attack_sound}; use super::hunger::HungerManager; use super::item::ItemEntity; use super::living::LivingEntity; use super::{Entity, EntityBase, NBTStorage, NBTStorageInit}; use pumpkin_data::potion::Effect; use pumpkin_world::chunk_system::ChunkLoading; const MAX_CACHED_SIGNATURES: u8 = 128; // Vanilla: 128 const MAX_PREVIOUS_MESSAGES: u8 = 20; // Vanilla: 20 pub const DATA_VERSION: i32 = 4790; // 26.1.2 struct HeapNode(i32, Vector2, SyncChunk); impl Eq for HeapNode {} impl PartialEq for HeapNode { fn eq(&self, other: &Self) -> bool { self.0 == other.0 } } impl PartialOrd for HeapNode { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } impl Ord for HeapNode { fn cmp(&self, other: &Self) -> std::cmp::Ordering { self.0.cmp(&other.0).reverse() } } pub struct ChunkManager { chunks_per_tick: usize, center: Vector2, view_distance: u8, chunk_listener: Receiver<(Vector2, SyncChunk)>, chunk_sent: HashMap, Weak>, chunk_queue: BinaryHeap, entity_chunk_queue: VecDeque<(Vector2, SyncEntityChunk)>, batches_sent_since_ack: u8, last_chunk_batch_sent_at: Instant, /// The current world for chunk loading. Updated on dimension change. world: Arc, } impl ChunkManager { pub const NOTCHIAN_BATCHES_WITHOUT_ACK_UNTIL_PAUSE: u8 = 10; const ACK_STALL_FALLBACK_DELAY: Duration = Duration::from_millis(250); #[must_use] pub fn new( chunks_per_tick: usize, chunk_listener: Receiver<(Vector2, SyncChunk)>, world: Arc, ) -> Self { Self { chunks_per_tick, center: Vector2::::new(0, 0), view_distance: 0, chunk_listener, chunk_sent: HashMap::new(), chunk_queue: BinaryHeap::new(), entity_chunk_queue: VecDeque::new(), batches_sent_since_ack: 0, last_chunk_batch_sent_at: Instant::now(), world, } } /// Gets the current world for chunk loading. #[must_use] pub const fn world(&self) -> &Arc { &self.world } fn should_enqueue_chunk(&mut self, position: Vector2, chunk: &SyncChunk) -> bool { self.chunk_sent .insert(position, Arc::downgrade(chunk)) .and_then(|old_chunk| old_chunk.upgrade()) .is_none_or(|old_chunk| !Arc::ptr_eq(&old_chunk, chunk)) } #[must_use] const fn ack_window_open(&self) -> bool { self.batches_sent_since_ack < Self::NOTCHIAN_BATCHES_WITHOUT_ACK_UNTIL_PAUSE } #[must_use] fn ack_fallback_ready(&self) -> bool { !self.ack_window_open() && self.last_chunk_batch_sent_at.elapsed() >= Self::ACK_STALL_FALLBACK_DELAY } pub fn pull_new_chunks(&mut self) { while let Ok((pos, chunk)) = self.chunk_listener.try_recv() { let dst = Self::chebyshev(pos, self.center); if dst > i32::from(self.view_distance) { continue; } if self.should_enqueue_chunk(pos, &chunk) { self.chunk_queue.push(HeapNode(dst, pos, chunk)); } } } fn chebyshev(a: Vector2, b: Vector2) -> i32 { (a.x - b.x).abs().max((a.y - b.y).abs()) } pub fn update_center_and_view_distance( &mut self, center: Vector2, mut view_distance: u8, level: &Arc, loading_chunks: &[Vector2], unloading_chunks: &[Vector2], ) { view_distance += 1; // Margin for loading let old_center = self.center; let old_view_distance = self.view_distance; { let mut lock = level.chunk_loading.lock().unwrap(); let new_level = ChunkLoading::get_level_from_view_distance(view_distance); lock.add_ticket(center, new_level); if old_center != center || old_view_distance != view_distance { let old_level = ChunkLoading::get_level_from_view_distance(old_view_distance); // Don't remove if it would be the same ticket if old_center != center || old_level != new_level { lock.remove_ticket(old_center, old_level); } } lock.send_change(); }; self.center = center; self.view_distance = view_distance; let view_distance_i32 = i32::from(view_distance); let unloading_chunks: HashSet> = unloading_chunks.iter().copied().collect(); self.chunk_sent.retain(|pos, _| { (pos.x - center.x).abs().max((pos.y - center.y).abs()) <= view_distance_i32 && !unloading_chunks.contains(pos) }); let mut tasks: Vec<_> = self .chunk_queue .drain() .filter_map(|node| { let dst = Self::chebyshev(node.1, center); (dst <= view_distance_i32 && !unloading_chunks.contains(&node.1)) .then(|| HeapNode(dst, node.1, node.2)) }) .collect(); for pos in loading_chunks { if !self.chunk_sent.contains_key(pos) && let Some(chunk) = level.loaded_chunks.get(pos) { let chunk = chunk.value().clone(); if self.should_enqueue_chunk(*pos, &chunk) { let dst = (pos.x - center.x).abs().max((pos.y - center.y).abs()); tasks.push(HeapNode(dst, *pos, chunk)); } } } self.chunk_queue = BinaryHeap::from(tasks); } pub fn clean_up(&mut self, level: &Arc) { let mut lock = level.chunk_loading.lock().unwrap(); lock.remove_ticket( self.center, ChunkLoading::get_level_from_view_distance(self.view_distance), ); lock.send_change(); let (_rx, tx) = crossbeam::channel::unbounded(); // drop old channel self.chunk_listener = tx; // Drop any held chunk references to allow chunks to be unloaded. self.chunk_sent.clear(); self.chunk_queue.clear(); self.entity_chunk_queue.clear(); self.batches_sent_since_ack = 0; self.last_chunk_batch_sent_at = Instant::now(); } pub fn change_world(&mut self, old_level: &Arc, new_world: Arc) { let mut lock = old_level.chunk_loading.lock().unwrap(); lock.remove_ticket( self.center, ChunkLoading::get_level_from_view_distance(self.view_distance), ); lock.send_change(); drop(lock); self.chunk_listener = new_world.level.chunk_listener.add_global_chunk_listener(); self.chunk_sent.clear(); self.chunk_queue.clear(); self.world = new_world; // Reset batch state so chunks can be sent immediately in the new dimension self.batches_sent_since_ack = 0; self.last_chunk_batch_sent_at = Instant::now(); } pub const fn handle_acknowledge(&mut self, chunks_per_tick: f32) { self.batches_sent_since_ack = 0; self.chunks_per_tick = chunks_per_tick.ceil() as usize; } pub fn push_chunk(&mut self, position: Vector2, chunk: SyncChunk) { if self.should_enqueue_chunk(position, &chunk) { let dst = (position.x - self.center.x) .abs() .max((position.y - self.center.y).abs()); self.chunk_queue.push(HeapNode(dst, position, chunk)); } } pub fn push_entity(&mut self, position: Vector2, chunk: SyncEntityChunk) { self.entity_chunk_queue.push_back((position, chunk)); } #[must_use] pub fn can_send_chunk(&self) -> bool { let state_available = self.ack_window_open() || self.ack_fallback_ready(); state_available && !self.chunk_queue.is_empty() } pub fn next_chunk(&mut self) -> Box<[SyncChunk]> { let take = self.chunk_queue.len().min(self.chunks_per_tick.max(1)); let mut chunks = Vec::with_capacity(take); for _ in 0..take { chunks.push(self.chunk_queue.pop().unwrap().2); } self.batches_sent_since_ack = self.batches_sent_since_ack.saturating_add(1); self.last_chunk_batch_sent_at = Instant::now(); chunks.into_boxed_slice() } pub fn next_entity(&mut self) -> Box<[SyncEntityChunk]> { let chunk_size = self .entity_chunk_queue .len() .min(self.chunks_per_tick.max(1)); let chunks: Box<[Arc]> = self .entity_chunk_queue .drain(..chunk_size) .map(|(_, chunk)| chunk) .collect(); self.batches_sent_since_ack = self.batches_sent_since_ack.saturating_add(1); self.last_chunk_batch_sent_at = Instant::now(); chunks } } /// Represents a Minecraft player entity. /// /// A `Player` is a special type of entity that represents a human player connected to the server. pub struct Player { /// The underlying living entity object that represents the player. pub living_entity: LivingEntity, /// The player's game profile information, including their username and UUID. pub gameprofile: GameProfile, /// The client connection associated with the player. pub client: ClientPlatform, /// The player's inventory. pub inventory: Arc, /// The player's `EnderChest` inventory. pub ender_chest_inventory: Arc, /// The player's configuration settings. Changes when the player changes their settings. pub config: ArcSwap, /// The player's current gamemode (e.g., Survival, Creative, Adventure). pub gamemode: AtomicCell, /// The player's previous gamemode pub previous_gamemode: AtomicCell>, /// The player's spawnpoint pub respawn_point: AtomicCell>, /// The player's sleep status pub sleeping_since: AtomicCell>, /// Manages the player's breath level pub breath_manager: BreathManager, /// Manages the player's hunger level. pub hunger_manager: HungerManager, /// The ID of the currently open container (if any). pub open_container: AtomicCell>, /// The block position of the currently open container screen (if any). pub open_container_pos: AtomicCell>, /// The item currently being held by the player. pub carried_item: Mutex>, /// The player's abilities and special powers. /// /// This field represents the various abilities that the player possesses, such as flight, invulnerability, and other special effects. /// /// **Note:** When the `abilities` field is updated, the server should send a `send_abilities_update` packet to the client to notify them of the changes. pub abilities: Mutex, /// The current stage of block destruction of the block the player is breaking. pub current_block_destroy_stage: AtomicI32, /// Indicates if the player is currently mining a block. pub mining: AtomicBool, pub start_mining_time: AtomicI32, pub tick_counter: AtomicI32, pub packet_sequence: AtomicI32, pub mining_pos: Mutex, /// A counter for teleport IDs used to track pending teleports. pub teleport_id_count: AtomicI32, /// The pending teleport information, including the teleport ID and target location. pub awaiting_teleport: Mutex)>>, /// The coordinates of the chunk section the player is currently watching. pub watched_section: AtomicCell, /// Whether we are waiting for a response after sending a keep alive packet. pub wait_for_keep_alive: AtomicBool, /// The keep alive packet payload we send. The client should respond with the same id. pub keep_alive_id: AtomicI64, /// The last time we sent a keep alive packet. pub last_keep_alive_time: AtomicCell, /// The last time the player performed an action (for idle timeout). pub last_action_time: AtomicCell, /// The ping in millis. pub ping: AtomicU32, /// The amount of ticks since the player's last attack. pub last_attacked_ticks: AtomicU32, /// The player's last known experience level. pub last_sent_xp: AtomicI32, pub last_sent_health: AtomicI32, pub last_sent_food: AtomicU8, pub last_food_saturation: AtomicBool, /// The player's permission level. pub permission_lvl: AtomicCell, /// Whether the client has reported that it has loaded. pub client_loaded: AtomicBool, /// The amount of time (in ticks) the client has to report having finished loading before being timed out. pub client_loaded_timeout: AtomicU32, /// Item usage tracking for bows, crossbows, etc. pub using_item: AtomicBool, pub item_use_start_time: AtomicI32, pub using_hand: AtomicCell>, /// The player's experience level. pub experience_level: AtomicI32, /// The player's experience progress (`0.0` to `1.0`) pub experience_progress: AtomicCell, /// The player's total experience points. pub experience_points: AtomicI32, pub experience_pick_up_delay: Mutex, pub chunk_manager: Mutex, pub has_played_before: AtomicBool, pub chat_session: Arc>, pub signature_cache: Mutex, pub player_screen_handler: Arc>, pub current_screen_handler: Mutex>>, pub screen_handler_sync_id: AtomicU8, pub screen_handler_listener: Arc, pub screen_handler_sync_handler: Arc, pub tab_list_header: Mutex, pub tab_list_footer: Mutex, pub display_name: Mutex>, pub tab_list_order: AtomicI32, pub tab_list_latency: AtomicI32, pub tab_list_listed: AtomicBool, } impl Player { #[expect(clippy::too_many_lines)] pub async fn new( client: ClientPlatform, gameprofile: GameProfile, config: PlayerConfig, world: Arc, gamemode: GameMode, ) -> Self { struct ScreenListener; impl ScreenHandlerListener for ScreenListener {} let server = world.server.upgrade().unwrap(); let player_uuid = gameprofile.id; let living_entity = LivingEntity::new(Entity::from_uuid( player_uuid, world.clone(), Vector3::new(0.0, 100.0, 0.0), &EntityType::PLAYER, )); living_entity.entity.invulnerable.store( matches!(gamemode, GameMode::Creative | GameMode::Spectator), Ordering::Relaxed, ); let inventory = Arc::new(PlayerInventory::new( living_entity.entity_equipment.clone(), living_entity.equipment_slots.clone(), )); let ender_chest_inventory = Arc::new(EnderChestInventory::new()); let player_screen_handler = Arc::new(Mutex::new( PlayerScreenHandler::new(&inventory, None, 0).await, )); // Initialize abilities based on gamemode (like vanilla's GameMode.setAbilities()) let mut abilities = Abilities::default(); abilities.set_for_gamemode(gamemode); Self { living_entity, config: ArcSwap::new(Arc::new(config)), gameprofile, client, awaiting_teleport: Mutex::new(None), breath_manager: BreathManager::default(), // TODO: Load this from previous instance hunger_manager: HungerManager::default(), current_block_destroy_stage: AtomicI32::new(-1), open_container: AtomicCell::new(None), open_container_pos: AtomicCell::new(None), tick_counter: AtomicI32::new(0), packet_sequence: AtomicI32::new(-1), start_mining_time: AtomicI32::new(0), carried_item: Mutex::new(None), experience_pick_up_delay: Mutex::new(0), teleport_id_count: AtomicI32::new(0), mining: AtomicBool::new(false), mining_pos: Mutex::new(BlockPos::ZERO), abilities: Mutex::new(abilities), gamemode: AtomicCell::new(gamemode), previous_gamemode: AtomicCell::new(None), // TODO: Send the CPlayerSpawnPosition packet when the client connects with proper values respawn_point: AtomicCell::new(None), sleeping_since: AtomicCell::new(None), // We want this to be an impossible watched section so that `chunker::update_position` // will mark chunks as watched for a new join rather than a respawn. // (We left shift by one so we can search around that chunk) watched_section: AtomicCell::new(Cylindrical::new( Vector2::new(0, 0), // Since 1 is not possible in vanilla it is used as uninit NonZeroU8::new(1).unwrap(), )), wait_for_keep_alive: AtomicBool::new(false), keep_alive_id: AtomicI64::new(0), last_keep_alive_time: AtomicCell::new(std::time::Instant::now()), last_action_time: AtomicCell::new(std::time::Instant::now()), ping: AtomicU32::new(0), last_attacked_ticks: AtomicU32::new(0), client_loaded: AtomicBool::new(false), client_loaded_timeout: AtomicU32::new(60), // Item usage tracking using_item: AtomicBool::new(false), item_use_start_time: AtomicI32::new(0), using_hand: AtomicCell::new(None), // Minecraft has no way to change the default permission level of new players. // Minecraft's default permission level is 0. permission_lvl: server .data .operator_config .read() .await .get_entry(&player_uuid) .map_or( AtomicCell::new(server.advanced_config.commands.default_op_level), |op| AtomicCell::new(op.level), ), inventory, ender_chest_inventory, experience_level: AtomicI32::new(0), experience_progress: AtomicCell::new(0.0), experience_points: AtomicI32::new(0), // Default to sending 16 chunks per tick. chunk_manager: Mutex::new(ChunkManager::new( 16, world.level.chunk_listener.add_global_chunk_listener(), world.clone(), )), last_sent_xp: AtomicI32::new(-1), last_sent_health: AtomicI32::new(-1), last_sent_food: AtomicU8::new(0), last_food_saturation: AtomicBool::new(true), has_played_before: AtomicBool::new(false), chat_session: Arc::new(Mutex::new(ChatSession::default())), // Placeholder value until the player actually sets their session id signature_cache: Mutex::new(MessageCache::default()), player_screen_handler: player_screen_handler.clone(), current_screen_handler: Mutex::new(player_screen_handler), screen_handler_sync_id: AtomicU8::new(0), screen_handler_listener: Arc::new(ScreenListener), screen_handler_sync_handler: Arc::new(SyncHandler::new()), tab_list_header: Mutex::new(TextComponent::text("")), tab_list_footer: Mutex::new(TextComponent::text("")), display_name: Mutex::new(None), tab_list_order: AtomicI32::new(0), tab_list_latency: AtomicI32::new(0), tab_list_listed: AtomicBool::new(false), } } pub async fn set_tab_list_header_footer(&self, header: TextComponent, footer: TextComponent) { *self.tab_list_header.lock().await = header.clone(); *self.tab_list_footer.lock().await = footer.clone(); self.client .enqueue_packet(&CTabList::new(&header, &footer)) .await; } pub async fn set_display_name(&self, display_name: Option) { *self.display_name.lock().await = display_name.clone(); // Update the tab list for everyone let world = self.world(); world .broadcast_packet_all(&CPlayerInfoUpdate::new( PlayerInfoFlags::UPDATE_DISPLAY_NAME.bits(), &[pumpkin_protocol::java::client::play::Player { uuid: self.gameprofile.id, actions: &[PlayerAction::UpdateDisplayName(display_name.as_ref())], }], )) .await; } pub async fn set_tab_list_order(&self, order: i32) { self.tab_list_order.store(order, Ordering::Relaxed); let world = self.world(); world .broadcast_packet_all(&CPlayerInfoUpdate::new( PlayerInfoFlags::UPDATE_LIST_PRIORITY.bits(), &[pumpkin_protocol::java::client::play::Player { uuid: self.gameprofile.id, actions: &[PlayerAction::UpdateListOrder(VarInt(order))], }], )) .await; } pub async fn set_tab_list_latency(&self, latency: i32) { self.tab_list_latency.store(latency, Ordering::Relaxed); let world = self.world(); world .broadcast_packet_all(&CPlayerInfoUpdate::new( PlayerInfoFlags::UPDATE_LATENCY.bits(), &[pumpkin_protocol::java::client::play::Player { uuid: self.gameprofile.id, actions: &[PlayerAction::UpdateLatency(VarInt(latency))], }], )) .await; } pub async fn set_tab_list_listed(&self, listed: bool) { self.tab_list_listed.store(listed, Ordering::Relaxed); let world = self.world(); world .broadcast_packet_all(&CPlayerInfoUpdate::new( PlayerInfoFlags::UPDATE_LISTED.bits(), &[pumpkin_protocol::java::client::play::Player { uuid: self.gameprofile.id, actions: &[PlayerAction::UpdateListed(listed)], }], )) .await; } /// Spawns a task associated with this player-client. All tasks spawned with this method are awaited /// when the client. This means tasks should complete in a reasonable amount of time or select /// on `Self::await_close_interrupt` to cancel the task when the client is closed /// /// Returns an `Option>`. If the client is closed, this returns `None`. pub fn spawn_task(&self, task: F) -> Option> where F: Future + Send + 'static, F::Output: Send + 'static, { self.client.spawn_task(task) } pub const fn inventory(&self) -> &Arc { &self.inventory } pub const fn ender_chest_inventory(&self) -> &Arc { &self.ender_chest_inventory } /// Removes the [`Player`] out of the current [`World`]. pub async fn remove(self: &Arc) { let world = self.world(); world.remove_player(self, true).await; let cylindrical = self.watched_section.load(); self.chunk_manager.lock().await.clean_up(&world.level); // Radial chunks are all of the chunks the player is theoretically viewing. // Given enough time, all of these chunks will be in memory. let radial_chunks = cylindrical.all_chunks_within(); debug!( "Removing player {}, unwatching {} chunks", self.gameprofile.name, radial_chunks.len() ); let level = &world.level; // Decrement the value of watched chunks let chunks_to_clean = level.mark_chunks_as_not_watched(&radial_chunks).await; // Remove chunks with no watchers from the cache level.clean_entity_chunks(&chunks_to_clean); // Remove left over entries from all possiblily loaded chunks level.clean_memory(); debug!( "Removed player id {} from world {} ({} chunks remain cached)", self.gameprofile.name, self.world().get_world_name(), level.loaded_chunk_count(), ); //self.world().level.list_cached(); } pub async fn attack(&self, victim: Arc) { let world = self.world(); let server = world.server.upgrade().unwrap(); let victim_entity = victim.get_entity(); let attacker_entity = &self.living_entity.entity; let config = &server.advanced_config.pvp; let inventory = self.inventory(); let item_stack = inventory.held_item(); let base_damage = self .living_entity .get_attribute_value(&Attributes::ATTACK_DAMAGE); let base_attack_speed = 4.0; let mut damage_multiplier = 1.0; let mut add_damage = 0.0; let mut add_speed = 0.0; // Get the attack damage from the held item // TODO: this should be cached in memory, we shouldn't just use default here either if let Some(modifiers) = item_stack .lock() .await .get_data_component::() { for item_mod in modifiers.attribute_modifiers.iter() { if item_mod.operation == Operation::AddValue { if item_mod.id == "minecraft:base_attack_damage" { add_damage = item_mod.amount; } else if item_mod.id == "minecraft:base_attack_speed" { add_speed = item_mod.amount; } } } } let attack_speed = base_attack_speed + add_speed; let attack_cooldown_progress = self.get_attack_cooldown_progress( f64::from(server.basic_config.tps), 0.5, attack_speed, ); self.last_attacked_ticks.store(0, Ordering::Relaxed); // Only reduce attack damage if in cooldown // TODO: Enchantments are reduced in the same way, just without the square. if attack_cooldown_progress < 1.0 { damage_multiplier = attack_cooldown_progress.powi(2).mul_add(0.8, 0.2); } // Modify the added damage based on the multiplier. let mut damage = base_damage + add_damage * damage_multiplier; let pos = victim_entity.pos.load(); let attack_type = AttackType::new(self, attack_cooldown_progress as f32).await; if matches!(attack_type, AttackType::Critical) { damage *= 1.5; } if !victim .damage_with_context( &*victim, damage as f32, DamageType::PLAYER_ATTACK, None, Some(self), Some(self), ) .await { world .play_sound( Sound::EntityPlayerAttackNodamage, SoundCategory::Players, &self.living_entity.entity.pos.load(), ) .await; return; } player_attack_sound(&pos, &world, attack_type).await; self.living_entity.last_attacking_id.store( victim_entity.entity_id, std::sync::atomic::Ordering::Relaxed, ); self.living_entity.last_attack_time.store( self.living_entity .entity .age .load(std::sync::atomic::Ordering::Relaxed), std::sync::atomic::Ordering::Relaxed, ); if victim.get_living_entity().is_some() { let mut knockback_strength = 1.0; match attack_type { AttackType::Knockback => knockback_strength += 1.0, AttackType::Sweeping => { combat::spawn_sweep_particle(attacker_entity, &world, &pos).await; } _ => {} } if config.knockback { combat::handle_knockback(attacker_entity, victim_entity, knockback_strength); } } // NOTE: TOCTOU race condition in single-player context. // The weapon cost is computed (cost = 1 or 2) with item_stack locked, then damage_held_item // re-acquires the lock. In async multi-task scenarios, another task could theoretically // swap the held item between these operations, causing the cost to apply to the wrong item. // Mitigation options (in priority order): // 1. Create damage_held_item_with_lock(&self, item_stack: MutexGuard, amount) variant // to hold the lock across both computation and application. // 2. Refactor compute cost as a closure: damage_held_item(self, |stack| -> i32 { ... }) // 3. In practice, single-player scenarios are safe (this is not multiplayer). Document // as a known limitation if refactoring is deemed too invasive. self.damage_held_item({ let stack = item_stack.lock().await; Self::combat_weapon_durability_cost(&stack) }) .await; if config.swing {} } /// Returns the durability cost for using the held item as a weapon in combat. /// Derived from the `Weapon` data component: items without it (e.g. shears, tools /// not designed for combat) take no durability damage on attack. /// Items with the component use its `item_damage_per_attack` value (default 1; /// axes, pickaxes, shovels, and hoes carry a value of 2). fn combat_weapon_durability_cost(stack: &ItemStack) -> i32 { stack .get_data_component::() .map_or(0, |w| w.item_damage_per_attack as i32) } pub async fn sync_hand_slot(&self, slot_index: usize, stack: ItemStack) { self.enqueue_slot_set_packet(&CSetPlayerInventory::new( (slot_index as i32).into(), &ItemStackSerializer::from(stack.clone()), )) .await; if slot_index == self.inventory.get_selected_slot() as usize { self.living_entity .send_equipment_changes(&[(EquipmentSlot::MAIN_HAND, stack)]) .await; } else if slot_index == PlayerInventory::OFF_HAND_SLOT { self.living_entity .send_equipment_changes(&[(EquipmentSlot::OFF_HAND, stack)]) .await; } } /// Applies `amount` durability damage to the item in `slot`. /// Broadcasts an [`EntityStatus`] break event and syncs the slot if the item is destroyed. pub async fn damage_item_in_slot(&self, slot: &EquipmentSlot, amount: i32) -> bool { if matches!( self.gamemode.load(), GameMode::Creative | GameMode::Spectator ) { return false; } // Direct PlayerInventory slot indices (matches build_equipment_slots). let slot_index: usize = match slot { EquipmentSlot::MainHand(_) => self.inventory.get_selected_slot() as usize, EquipmentSlot::OffHand(_) => PlayerInventory::OFF_HAND_SLOT, // 40 EquipmentSlot::Feet(_) => 36, EquipmentSlot::Legs(_) => 37, EquipmentSlot::Chest(_) => 38, EquipmentSlot::Head(_) => 39, // Players do not have Body or Saddle equipment slots; // these are only used by non-player entities (e.g. horses). EquipmentSlot::Body(_) | EquipmentSlot::Saddle(_) => return false, }; let stack_arc = self.inventory.get_stack(slot_index).await; let updated = { let mut stack = stack_arc.lock().await; let result = stack.damage_item(amount); (result != pumpkin_data::item_stack::DamageResult::Untouched) .then_some((result, stack.clone())) }; if let Some((result, updated_stack)) = updated { // Send the break status before clearing the slot so the client can // use the item texture for break particles. if result == pumpkin_data::item_stack::DamageResult::Broken { self.world() .send_entity_status( &self.living_entity.entity, super::equipment_break_status(slot), ) .await; } self.enqueue_slot_set_packet(&CSetPlayerInventory::new( (slot_index as i32).into(), &ItemStackSerializer::from(updated_stack.clone()), )) .await; self.living_entity .send_equipment_changes(&[(slot.clone(), updated_stack)]) .await; return true; } false } /// Convenience wrapper – damages the currently held (main-hand) item. pub async fn damage_held_item(&self, amount: i32) -> bool { self.damage_item_in_slot(&EquipmentSlot::MAIN_HAND, amount) .await } pub async fn apply_tool_damage_for_block_break(&self, state: &BlockState) { if matches!( self.gamemode.load(), GameMode::Creative | GameMode::Spectator ) { return; } if state.hardness <= 0.0 { return; } let damage = { let stack = self.inventory.held_item(); let stack = stack.lock().await; stack .get_data_component::() .map_or(0, |tool| tool.damage_per_block as i32) }; if damage > 0 { self.damage_held_item(damage).await; } } pub async fn set_respawn_point( &self, dimension: Dimension, block_pos: BlockPos, yaw: f32, pitch: f32, ) -> bool { if let Some(respawn_point) = self.respawn_point.load() && dimension == respawn_point.dimension && block_pos == respawn_point.position { return false; } self.respawn_point.store(Some(RespawnPoint { dimension, position: block_pos, yaw, force: false, })); self.client .send_packet_now(&CPlayerSpawnPosition::new( block_pos, yaw, pitch, dimension.minecraft_name.to_owned(), )) .await; true } /// Sets the respawn point with force=true, bypassing bed/anchor checks. /// Used by /spawnpoint command. pub async fn set_respawn_point_forced( &self, dimension: Dimension, block_pos: BlockPos, yaw: f32, pitch: f32, ) { self.respawn_point.store(Some(RespawnPoint { dimension, position: block_pos, yaw, force: true, })); self.client .send_packet_now(&CPlayerSpawnPosition::new( block_pos, yaw, pitch, dimension.minecraft_name.to_owned(), )) .await; } /// Calculates the player's respawn point based on stored spawn data. /// /// Returns `Some(CalculatedRespawnPoint)` if a valid respawn point exists, `None` otherwise. /// /// # Behavior /// - If `force` flag is set (via `/spawnpoint` command), validates the spawn position is safe /// (both the block and block above allow mob spawn). /// - For beds: validates the bed block still exists and finds a valid spawn position around it. /// - For respawn anchors (Nether): validates the anchor has charges and finds a valid spawn position. /// - Returns `None` if the spawn block is invalid/missing (caller should send /// `NoRespawnBlockAvailable` game event and use world spawn). /// /// # Note /// This function does NOT send any packets. The caller is responsible for /// sending `NoRespawnBlockAvailable` if this returns `None`. pub async fn calculate_respawn_point(&self) -> Option { type BedProperties = pumpkin_data::block_properties::WhiteBedLikeProperties; type AnchorProperties = pumpkin_data::block_properties::RespawnAnchorLikeProperties; let respawn_point = self.respawn_point.load()?; let world = self.world(); let pos = &respawn_point.position; let (block, state_id) = world.get_block_and_state_id(pos).await; // If force is set (from /spawnpoint command), validate position is safe if respawn_point.force { // For forced spawn, check if both the block and block above allow mob spawn let block_state = world.get_block_state(pos).await; let above_state = world.get_block_state(&pos.up()).await; // Check if blocks are passable (non-solid or air) let block_safe = block_state.is_air() || !block_state.is_solid(); let above_safe = above_state.is_air() || !above_state.is_solid(); if block_safe && above_safe { let position = Vector3::new( f64::from(pos.0.x) + 0.5, f64::from(pos.0.y) + 0.1, f64::from(pos.0.z) + 0.5, ); debug!( "Returning forced spawn point at {:?}, dimension: {:?}", position, respawn_point.dimension ); return Some(CalculatedRespawnPoint { position, yaw: respawn_point.yaw, pitch: 0.0, dimension: respawn_point.dimension, }); } return None; } // Handle bed respawn if block.has_tag(&tag::Block::MINECRAFT_BEDS) { let bed_props = BedProperties::from_state_id(state_id, block); let facing = bed_props.facing; // Try positions around the bed based on facing direction // Vanilla tries multiple offset patterns; we use a simplified version if let Some(spawn_pos) = Self::find_bed_spawn_position(&world, pos, facing, respawn_point.yaw).await { return Some(CalculatedRespawnPoint { position: spawn_pos, yaw: respawn_point.yaw, pitch: 0.0, dimension: respawn_point.dimension, }); } return None; } // Handle respawn anchor (Nether) if block == &Block::RESPAWN_ANCHOR { let anchor_props = AnchorProperties::from_state_id(state_id, block); let charges = anchor_props.charges; // Anchor needs at least 1 charge to work if charges == 0 { return None; } // Try positions around the anchor if let Some(spawn_pos) = Self::find_anchor_spawn_position(&world, pos).await { // Decrement charges after successful respawn position found let new_charges = charges - 1; let mut new_props = anchor_props; new_props.charges = new_charges; world .set_block_state( pos, new_props.to_state_id(block), pumpkin_world::world::BlockFlags::NOTIFY_ALL, ) .await; return Some(CalculatedRespawnPoint { position: spawn_pos, yaw: respawn_point.yaw, pitch: 0.0, dimension: respawn_point.dimension, }); } return None; } None } /// Find a valid spawn position around a bed. /// Vanilla uses a complex algorithm based on bed facing direction. /// We use a simplified version that tries cardinal directions first. async fn find_bed_spawn_position( world: &Arc, bed_pos: &BlockPos, facing: HorizontalFacing, _spawn_angle: f32, ) -> Option> { // Get offsets based on bed facing direction (vanilla-like order) let offsets = Self::get_bed_spawn_offsets(facing); for (dx, dz) in offsets { let check_pos = BlockPos(Vector3::new( bed_pos.0.x + dx, bed_pos.0.y, bed_pos.0.z + dz, )); if let Some(pos) = Self::find_respawn_pos(world, &check_pos).await { return Some(pos); } // Also try one block down (for beds on elevated platforms) let check_pos_down = BlockPos(Vector3::new( bed_pos.0.x + dx, bed_pos.0.y - 1, bed_pos.0.z + dz, )); if let Some(pos) = Self::find_respawn_pos(world, &check_pos_down).await { return Some(pos); } } // Try on the bed itself as last resort if let Some(pos) = Self::find_respawn_pos(world, bed_pos).await { return Some(pos); } None } /// Get spawn position offsets around a bed based on facing direction. /// This is a simplified version of vanilla's getAroundBedOffsets. fn get_bed_spawn_offsets(facing: HorizontalFacing) -> Vec<(i32, i32)> { let (fx, fz) = match facing { HorizontalFacing::North => (0, -1), HorizontalFacing::South => (0, 1), HorizontalFacing::West => (-1, 0), HorizontalFacing::East => (1, 0), }; // Clockwise rotation let (rx, rz) = (-fz, fx); vec![ (rx, rz), // Right of bed (-rx, -rz), // Left of bed (rx - fx, rz - fz), // Right-back (-rx - fx, -rz - fz), // Left-back (-fx, -fz), // Behind foot (-fx * 2, -fz * 2), // Further behind (rx + fx, rz + fz), // Right-front (-rx + fx, -rz + fz), // Left-front (fx, fz), // In front (rx - fx * 2, rz - fz * 2), // Far right-back ] } /// Find a valid spawn position around a respawn anchor. async fn find_anchor_spawn_position( world: &Arc, anchor_pos: &BlockPos, ) -> Option> { // Vanilla VALID_HORIZONTAL_SPAWN_OFFSETS let horizontal_offsets: [(i32, i32); 8] = [ (0, -1), (-1, 0), (0, 1), (1, 0), (-1, -1), (1, -1), (-1, 1), (1, 1), ]; // Try at same level, then one down, then one up for dy in [0, -1, 1] { for (dx, dz) in horizontal_offsets { let check_pos = BlockPos(Vector3::new( anchor_pos.0.x + dx, anchor_pos.0.y + dy, anchor_pos.0.z + dz, )); if let Some(pos) = Self::find_respawn_pos(world, &check_pos).await { return Some(pos); } } } // Also try directly above the anchor let above_pos = anchor_pos.up(); Self::find_respawn_pos(world, &above_pos).await } /// Check if a position is valid for respawning (vanilla Dismounting.findRespawnPos logic). /// Returns the spawn position if valid, None otherwise. async fn find_respawn_pos( world: &Arc, pos: &BlockPos, ) -> Option> { let state = world.get_block_state(pos).await; let below_state = world.get_block_state(&pos.down()).await; // Check if block at position is invalid for spawn (e.g., inside solid block) let block = world.get_block(pos).await; if block.has_tag(&tag::Block::MINECRAFT_INVALID_SPAWN_INSIDE) { return None; } // Check if block above is also invalid let above_block = world.get_block(&pos.up()).await; if above_block.has_tag(&tag::Block::MINECRAFT_INVALID_SPAWN_INSIDE) { return None; } // Need solid floor below or at position let has_floor = below_state.is_solid() || state.is_solid(); if !has_floor { return None; } // Position must not be inside a solid block if state.is_solid() && !state.is_air() { return None; } // Create player-sized bounding box at this position let x = f64::from(pos.0.x) + 0.5; let y = f64::from(pos.0.y) + 0.1; let z = f64::from(pos.0.z) + 0.5; let spawn_pos = Vector3::new(x, y, z); // Player dimensions: 0.6 wide, 1.8 tall let half_width = 0.3; let height = 1.8; let player_box = BoundingBox::new( Vector3::new(x - half_width, y, z - half_width), Vector3::new(x + half_width, y + height, z + half_width), ); // Check if the space is empty (no block collisions) if !world.is_space_empty(player_box).await { return None; } Some(spawn_pos) } pub async fn sleep(&self, bed_head_pos: BlockPos) { // TODO: Stop riding self.get_entity().set_pose(EntityPose::Sleeping).await; self.living_entity .entity .set_pos(bed_head_pos.to_f64().add_raw(0.5, 0.6875, 0.5)); self.get_entity() .send_meta_data(&[Metadata::new( TrackedData::SLEEPING_POS_ID, MetaDataType::OPTIONAL_BLOCK_POS, Some(bed_head_pos), )]) .await; self.get_entity().set_velocity(Vector3::default()).await; self.sleeping_since.store(Some(0)); } pub async fn get_off_ground_speed(&self) -> f64 { let sprinting = self.get_entity().sprinting.load(Ordering::Relaxed); if !self.get_entity().has_vehicle().await { let fly_speed = { let abilities = self.abilities.lock().await; abilities.flying.then_some(f64::from(abilities.fly_speed)) }; if let Some(flying) = fly_speed { return if sprinting { flying * 2.0 } else { flying }; } } if sprinting { 0.025_999_999 } else { 0.02 } } pub async fn is_flying(&self) -> bool { let abilities = self.abilities.lock().await; abilities.flying } fn is_sleeping(&self) -> bool { // TODO: Track sleeping position state explicitly (vanilla checks sleepingPosition.isPresent()). self.sleeping_since.load().is_some() } async fn is_swimming(&self, flying: bool) -> bool { let entity = self.get_entity(); let swim_height = self.living_entity.get_swim_height(); // TODO: Replace this inferred check with vanilla-equivalent swimming state tracking // (LivingEntity#updateSwimming + entity swimming flag). entity.touching_water.load(Ordering::Relaxed) && entity.water_height.load() > swim_height && entity.sprinting.load(Ordering::Relaxed) && !entity.on_ground.load(Ordering::Relaxed) && !flying && !entity.has_vehicle().await } const fn is_auto_spin_attack() -> bool { // TODO: Track active auto-spin/riptide state and return true while it is active. false } async fn can_fit_pose(&self, pose: EntityPose) -> bool { let entity = self.get_entity(); let dimensions = Entity::get_entity_dimensions(pose); let position = entity.pos.load(); let aabb = BoundingBox::new_from_pos(position.x, position.y, position.z, &dimensions); entity .world .load() .is_space_empty(aabb.contract_all(1.0E-7)) .await } pub async fn update_player_pose(&self) { let entity = self.get_entity(); if !self.can_fit_pose(EntityPose::Swimming).await { return; } let flying = self.is_flying().await; let desired_pose = if self.is_sleeping() { EntityPose::Sleeping } else if self.is_swimming(flying).await { EntityPose::Swimming } else if entity.fall_flying.load(Ordering::Relaxed) { EntityPose::FallFlying } else if Self::is_auto_spin_attack() { EntityPose::SpinAttack } else if entity.sneaking.load(Ordering::Relaxed) && !flying { EntityPose::Crouching } else { EntityPose::Standing }; let new_pose = if self.gamemode.load() == GameMode::Spectator || entity.has_vehicle().await || self.can_fit_pose(desired_pose).await { desired_pose } else if self.can_fit_pose(EntityPose::Crouching).await { EntityPose::Crouching } else { EntityPose::Swimming }; if entity.pose.load() != new_pose { entity.set_pose(new_pose).await; } } pub async fn wake_up(&self) { let world = self.world(); let respawn_point = self .respawn_point .load() .expect("Player waking up should have it's respawn point set on the bed."); let (bed, bed_state) = world.get_block_and_state_id(&respawn_point.position).await; BedBlock::set_occupied(false, &world, bed, &respawn_point.position, bed_state).await; self.living_entity .entity .set_pose(EntityPose::Standing) .await; self.living_entity.entity.set_pos(self.position()); self.living_entity .entity .send_meta_data(&[Metadata::new( TrackedData::SLEEPING_POS_ID, MetaDataType::OPTIONAL_BLOCK_POS, None::, )]) .await; let chunk_pos = self.living_entity.entity.chunk_pos.load(); world .broadcast_to_chunk( chunk_pos, &CEntityAnimation::new(self.entity_id().into(), Animation::LeaveBed), ) .await; self.sleeping_since.store(None); } pub async fn show_title(&self, text: &TextComponent, mode: &TitleMode) { match mode { TitleMode::Title => self.client.enqueue_packet(&CTitleText::new(text)).await, TitleMode::SubTitle => self.client.enqueue_packet(&CSubtitle::new(text)).await, TitleMode::ActionBar => self.client.enqueue_packet(&CActionBar::new(text)).await, } } pub async fn send_title_animation(&self, fade_in: i32, stay: i32, fade_out: i32) { self.client .enqueue_packet(&CTitleAnimation::new(fade_in, stay, fade_out)) .await; } pub async fn spawn_particle( &self, position: Vector3, offset: Vector3, max_speed: f32, particle_count: i32, particle: Particle, ) { self.client .enqueue_packet(&CParticle::new( false, false, position, offset, max_speed, particle_count, VarInt(particle as i32), &[], )) .await; } pub async fn play_sound( &self, sound_id: u16, category: SoundCategory, position: &Vector3, volume: f32, pitch: f32, seed: f64, ) { self.client .enqueue_packet(&CSoundEffect::new( IdOr::Id(sound_id), category, position, volume, pitch, seed, )) .await; } pub async fn play_sound_event( &self, sound: SoundEvent, category: SoundCategory, position: &Vector3, volume: f32, pitch: f32, seed: f64, ) { self.client .enqueue_packet(&CSoundEffect::new( IdOr::Value(sound), category, position, volume, pitch, seed, )) .await; } /// Stops a sound playing on the client. /// /// # Arguments /// /// * `sound_id`: An optional [`ResourceLocation`] specifying the sound to stop. If [`None`], all sounds in the specified category (if any) will be stopped. /// * `category`: An optional [`SoundCategory`] specifying the sound category to stop. If [`None`], all sounds with the specified resource location (if any) will be stopped. pub async fn stop_sound( &self, sound_id: Option, category: Option, ) { self.client .enqueue_packet(&CStopSound::new(sound_id, category)) .await; } // TODO Abstract the chunk sending #[expect(clippy::too_many_lines)] pub async fn tick(self: &Arc, server: &Server) { self.current_screen_handler .lock() .await .lock() .await .send_content_updates() .await; // if self.client.closed.load(Ordering::Relaxed) { // return; // } let seq = self.packet_sequence.swap(-1, Ordering::Relaxed); if seq != -1 { self.client .send_packet_now(&CAcknowledgeBlockChange::new(seq.into())) .await; } { let mut xp = self.experience_pick_up_delay.lock().await; if *xp > 0 { *xp -= 1; } } let chunk_of_chunks = { let mut chunk_manager = self.chunk_manager.lock().await; chunk_manager.pull_new_chunks(); if let ClientPlatform::Java(_) = self.client { // Java clients can only send a limited amount of chunks per tick. // If we have sent too many chunks without receiving an ack, we stop sending chunks. chunk_manager .can_send_chunk() .then(|| chunk_manager.next_chunk()) } else { Some(chunk_manager.next_chunk()) } }; if let Some(chunk_of_chunks) = chunk_of_chunks { let chunk_count = chunk_of_chunks.len(); match &self.client { ClientPlatform::Java(java_client) => { java_client.send_packet_now(&CChunkBatchStart).await; for chunk in chunk_of_chunks { // log::debug!("send chunk {:?}", chunk.position); // TODO: Can we check if we still need to send the chunk? Like if it's a fast moving // player or something. java_client.send_packet_now(&CChunkData(&chunk)).await; } java_client .send_packet_now(&CChunkBatchEnd::new(chunk_count as u16)) .await; } ClientPlatform::Bedrock(bedrock_client) => { for chunk in chunk_of_chunks { bedrock_client .send_game_packet(&CLevelChunk { dimension: 0, cache_enabled: false, chunk: &chunk, }) .await; } } } } self.tick_counter.fetch_add(1, Ordering::Relaxed); self.living_entity .entity .age .fetch_add(1, Ordering::Relaxed); if let Some(sleeping_since) = self.sleeping_since.load() && sleeping_since < 101 { self.sleeping_since.store(Some(sleeping_since + 1)); } if self.mining.load(Ordering::Relaxed) { let pos = self.mining_pos.lock().await; let world = self.world(); let state = world.get_block_state(&pos).await; // Is the block broken? if state.is_air() { world .set_block_breaking(&self.living_entity.entity, *pos, -1) .await; self.current_block_destroy_stage .store(-1, Ordering::Relaxed); self.mining.store(false, Ordering::Relaxed); } else { self.continue_mining( *pos, &world, state, self.start_mining_time.load(Ordering::Relaxed), ) .await; } } self.last_attacked_ticks.fetch_add(1, Ordering::Relaxed); let caller: Arc = self.clone(); self.living_entity.tick(&caller, server).await; // Vanilla updates pose in PlayerEntity#tick after super.tick(). self.update_player_pose().await; self.breath_manager.tick(self).await; self.hunger_manager.tick(self).await; // experience handling self.tick_experience().await; self.tick_health().await; // Timeout/keep alive handling self.tick_client_load_timeout(); // Idle timeout handling let now = Instant::now(); let idle_timeout_minutes = server.player_idle_timeout.load(Ordering::Relaxed); if idle_timeout_minutes > 0 { let idle_duration = now.duration_since(self.last_action_time.load()); if idle_duration >= Duration::from_secs(idle_timeout_minutes as u64 * 60) { self.kick( DisconnectReason::KickedForIdle, TextComponent::translate(translation::MULTIPLAYER_DISCONNECT_IDLING, []), ) .await; return; } } // TODO This should only be handled by the ClientPlatform if now.duration_since(self.last_keep_alive_time.load()) >= Duration::from_secs(15) { if matches!(self.client, ClientPlatform::Bedrock(_)) { return; } // We never got a response from the last keep alive we sent. if self.wait_for_keep_alive.load(Ordering::Relaxed) { self.kick( DisconnectReason::Timeout, TextComponent::translate(translation::DISCONNECT_TIMEOUT, []), ) .await; return; } self.wait_for_keep_alive.store(true, Ordering::Relaxed); self.last_keep_alive_time.store(now); let id = now.elapsed().as_millis() as i64; self.keep_alive_id.store(id, Ordering::Relaxed); self.client.enqueue_packet(&CKeepAlive::new(id)).await; } } async fn continue_mining( &self, location: BlockPos, world: &World, state: &BlockState, starting_time: i32, ) { let time = self.tick_counter.load(Ordering::Relaxed) - starting_time; let speed = block::calc_block_breaking(self, state, Block::from_state_id(state.id)).await * (time + 1) as f32; let progress = (speed * 10.0) as i32; if progress != self.current_block_destroy_stage.load(Ordering::Relaxed) { world .set_block_breaking(&self.living_entity.entity, location, progress) .await; self.current_block_destroy_stage .store(progress, Ordering::Relaxed); } } pub async fn jump(&self) { if self.living_entity.entity.sprinting.load(Ordering::Relaxed) { self.add_exhaustion(0.2).await; } else { self.add_exhaustion(0.05).await; } } pub async fn progress_motion(&self, delta_pos: Vector3) { // TODO: Swimming, gliding... if self.living_entity.entity.on_ground.load(Ordering::Relaxed) { let delta = (delta_pos.horizontal_length() * 100.0).round() as f32; if delta > 0.0 { if self.living_entity.entity.sprinting.load(Ordering::Relaxed) { self.add_exhaustion(0.1 * delta * 0.01).await; } else { self.add_exhaustion(0.0 * delta * 0.01).await; } } } } pub fn has_client_loaded(&self) -> bool { self.client_loaded.load(Ordering::Relaxed) || self.client_loaded_timeout.load(Ordering::Relaxed) == 0 } pub fn set_client_loaded(&self, loaded: bool) { if !loaded { self.client_loaded_timeout.store(60, Ordering::Relaxed); } self.client_loaded.store(loaded, Ordering::Relaxed); } pub fn get_attack_cooldown_progress(&self, tps: f64, base_time: f64, attack_speed: f64) -> f64 { let x = f64::from(self.last_attacked_ticks.load(Ordering::Acquire)) + base_time; let progress_per_tick = tps / attack_speed; let progress = x / progress_per_tick; progress.clamp(0.0, 1.0) } pub const fn entity_id(&self) -> i32 { self.living_entity.entity.entity_id } pub fn world(&self) -> Arc { self.living_entity.entity.world.load_full() } pub fn position(&self) -> Vector3 { self.living_entity.entity.pos.load() } pub fn eye_position(&self) -> Vector3 { let eye_height = self.living_entity.entity.get_eye_height(); Vector3::new( self.living_entity.entity.pos.load().x, self.living_entity.entity.pos.load().y + eye_height, self.living_entity.entity.pos.load().z, ) } /// Returns the player's rotation. /// Yaw then Pitch pub fn rotation(&self) -> (f32, f32) { ( self.living_entity.entity.yaw.load(), self.living_entity.entity.pitch.load(), ) } /// Updates the current abilities the player has. pub async fn send_abilities_update(&self) { match &self.client { ClientPlatform::Java(java) => { let mut b = 0; let abilities = &self.abilities.lock().await; if abilities.invulnerable { b |= 1; } if abilities.flying { b |= 2; } if abilities.allow_flying { b |= 4; } if abilities.creative { b |= 8; } java.enqueue_packet(&CPlayerAbilities::new( b, abilities.fly_speed, abilities.walk_speed, )) .await; } ClientPlatform::Bedrock(bedrock) => { let abilities = self.abilities.lock().await; let is_op = self.permission_lvl.load() == PermissionLvl::Four; let is_spectator = self.gamemode.load() == GameMode::Spectator; // 1. Permission Mapping let player_perm = if is_op { 2 } else { 1 }; // 1: Member, 2: Operator let command_perm = u8::from(is_op); // 0: Normal, 1: Operator // 2. Build the Ability Bitmask let mut ability_value: u32 = 0; // Helper closure to set bits using your enum let mut set_ability = |ability: Ability, enabled: bool| { if enabled { ability_value |= 1 << (ability as u32); } }; // Base Permissions set_ability(Ability::MayFly, abilities.allow_flying); set_ability(Ability::Flying, abilities.flying); set_ability( Ability::Invulnerable, abilities.invulnerable || abilities.creative, ); // Operator Specifics set_ability(Ability::OperatorCommands, is_op); set_ability(Ability::Teleport, is_op); // Interaction Permissions (Disabled for Spectators) let can_interact = !is_spectator; set_ability(Ability::Build, can_interact); set_ability(Ability::Mine, can_interact); set_ability(Ability::DoorsAndSwitches, can_interact); set_ability(Ability::OpenContainers, can_interact); set_ability(Ability::AttackPlayers, can_interact); set_ability(Ability::AttackMobs, can_interact); // Creative/Spectator Extras set_ability(Ability::Instabuild, abilities.creative); set_ability(Ability::NoClip, is_spectator); // 3. Construct the Layers let mut layers = vec![AbilityLayer { serialized_layer: 0, // LAYER_BASE // 0x3FFFF defines the first 18 bits as "provided" by this packet abilities_set: (1 << Ability::AbilityCount as u32) - 1, ability_value, fly_speed: 0.05, vertical_fly_speed: 1.0, walk_speed: 0.1, }]; if is_spectator { layers.push(AbilityLayer { serialized_layer: 1, abilities_set: 1 << (Ability::Flying as u32), ability_value: 1 << (Ability::Flying as u32), fly_speed: 0.05, vertical_fly_speed: 1.0, walk_speed: 0.1, }); } let packet = CUpdateAbilities { target_player_raw_id: self.entity_id().into(), player_permission: player_perm, command_permission: command_perm, layers, }; bedrock.send_game_packet(&packet).await; } } } /// Updates the client of the player's current permission level. pub async fn send_permission_lvl_update(&self) { let status = match self.permission_lvl.load() { PermissionLvl::Zero => EntityStatus::PermissionLevelAll, PermissionLvl::One => EntityStatus::PermissionLevelModerators, PermissionLvl::Two => EntityStatus::PermissionLevelGamemasters, PermissionLvl::Three => EntityStatus::PermissionLevelAdmins, PermissionLvl::Four => EntityStatus::PermissionLevelOwners, }; self.world() .send_entity_status(&self.living_entity.entity, status) .await; } /// Sets the player's difficulty level. pub async fn send_difficulty_update(&self) { let world = self.world(); let level_info = world.level_info.load(); self.client .enqueue_packet(&CChangeDifficulty::new( level_info.difficulty as u8, level_info.difficulty_locked, )) .await; } /// Sets the player's permission level and notifies the client. pub async fn set_permission_lvl( self: &Arc, server: &Server, lvl: PermissionLvl, command_dispatcher: &CommandDispatcher, ) { self.permission_lvl.store(lvl); self.send_permission_lvl_update().await; client_suggestions::send_c_commands_packet(self, server, command_dispatcher).await; } /// Sends the world time to only this player. pub async fn send_time(&self, world: &World) { let l_world = world.level_time.lock().await; match &self.client { ClientPlatform::Java(java_client) => { java_client .enqueue_packet(&CUpdateTime::new( l_world.world_age, l_world.time_of_day, true, )) .await; } ClientPlatform::Bedrock(bedrock_client) => { bedrock_client .send_game_packet(&CSetTime { time: VarInt(l_world.query_daytime() as _), }) .await; } } } pub async fn unload_watched_chunks(&self, world: &World) { let radial_chunks = self.watched_section.load().all_chunks_within(); let level = &world.level; let chunks_to_clean = level.mark_chunks_as_not_watched(&radial_chunks).await; // level.clean_chunks(&chunks_to_clean).await; for chunk in chunks_to_clean { self.client .enqueue_packet(&CUnloadChunk::new(chunk.x, chunk.y)) .await; } self.watched_section.store(Cylindrical::new( Vector2::new(0, 0), NonZeroU8::new(1).unwrap(), )); } /// Teleports the player to a different world or dimension with an optional position, yaw, and pitch. pub async fn teleport_world( self: &Arc, new_world: Arc, position: Vector3, yaw: Option, pitch: Option, ) { let current_world = self.living_entity.entity.world.load_full(); let yaw = yaw.unwrap_or(new_world.level_info.load().spawn_yaw); let pitch = pitch.unwrap_or(new_world.level_info.load().spawn_pitch); let server = new_world.server.upgrade().unwrap(); send_cancellable! {{ server; PlayerChangeWorldEvent { player: self.clone(), previous_world: current_world.clone(), new_world: new_world.clone(), position, yaw, pitch, cancelled: false, }; 'after: { // TODO: this is duplicate code from world let position = event.position; let yaw = event.yaw; let pitch = event.pitch; let new_world = event.new_world; self.set_client_loaded(false); let player = current_world.remove_player(self, false).await.unwrap(); new_world.players.rcu(|current_list| { let mut new_list = (**current_list).clone(); new_list.push(player.clone()); new_list }); self.unload_watched_chunks(¤t_world).await; self.chunk_manager.lock().await.change_world(¤t_world.level, new_world.clone()); // Update the entity's world reference for correct dimension-based operations self.living_entity.entity.set_world(new_world.clone()); let last_pos = self.living_entity.entity.last_pos.load(); let death_dimension = ResourceLocation::from(self.world().dimension.minecraft_name); let death_location = BlockPos(Vector3::new( last_pos.x.round() as i32, last_pos.y.round() as i32, last_pos.z.round() as i32, )); self.client .send_packet_now(&CRespawn::new( (new_world.dimension.id).into(), new_world.dimension.minecraft_name.to_string(), biome::hash_seed(new_world.level.seed.0), // seed self.gamemode.load() as u8, self.gamemode.load() as i8, false, false, Some((death_dimension, death_location)), VarInt(self.get_entity().portal_cooldown.load(Ordering::Relaxed) as i32), new_world.sea_level.into(), 1, )).await; self.send_permission_lvl_update().await; player.clone().request_teleport(position, yaw, pitch).await; player.living_entity.entity.last_pos.store(position); self.send_abilities_update().await; self.enqueue_set_held_item_packet(&CSetSelectedSlot::new( self.get_inventory().get_selected_slot() as i8, )).await; self.on_screen_handler_opened(self.player_screen_handler.clone()).await; self.send_health().await; new_world.send_world_info(&player, position, yaw, pitch).await; } }} } /// `yaw` and `pitch` are in degrees. /// Rarly used, for example when waking up the player from a bed or their first time spawn. Otherwise, the `teleport` method should be used. /// The player should respond with the `SConfirmTeleport` packet. pub async fn request_teleport(self: &Arc, position: Vector3, yaw: f32, pitch: f32) { // This is the ultra special magic code used to create the teleport id // This returns the old value // This operation wraps around on overflow. let server = self.world().server.upgrade().unwrap(); send_cancellable! {{ server; PlayerTeleportEvent { player: self.clone(), from: self.living_entity.entity.pos.load(), to: position, cancelled: false, }; 'after: { let position = event.to; let i = self .teleport_id_count .fetch_add(1, Ordering::Relaxed); let teleport_id = i + 1; self.living_entity.entity.set_pos(position); let entity = &self.living_entity.entity; entity.set_rotation(yaw, pitch); *self.awaiting_teleport.lock().await = Some((teleport_id.into(), position)); self.client .send_packet_now(&CPlayerPosition::new( teleport_id.into(), position, Vector3::new(0.0, 0.0, 0.0), yaw, pitch, // TODO Vec::new(), )).await; } }} } pub fn block_interaction_range(&self) -> f64 { if self.gamemode.load() == GameMode::Creative { 5.0 } else { 4.5 } } pub fn can_interact_with_block_at(&self, position: &BlockPos, additional_range: f64) -> bool { let d = self.block_interaction_range() + additional_range; let box_pos = BoundingBox::from_block(position); let entity_pos = self.living_entity.entity.pos.load(); let eye_height = self.living_entity.entity.get_eye_height(); box_pos.squared_magnitude(Vector3 { x: entity_pos.x, y: entity_pos.y + eye_height, z: entity_pos.z, }) < d * d } pub async fn kick(&self, reason: DisconnectReason, message: TextComponent) { self.client.kick(reason, message).await; } /// Updates the last action time to now. Call this on player actions like movement, chat, etc. pub fn update_last_action_time(&self) { self.last_action_time.store(std::time::Instant::now()); } pub fn can_food_heal(&self) -> bool { let health = self.living_entity.health.load(); let max_health = self.living_entity.get_max_health(); health > 0.0 && health < max_health } pub async fn add_exhaustion(&self, exhaustion: f32) { if self.abilities.lock().await.invulnerable { return; } self.hunger_manager.add_exhaustion(exhaustion); } pub async fn heal(&self, additional_health: f32) { self.living_entity.heal(additional_health).await; self.send_health().await; } pub async fn send_health(&self) { if !self.has_client_loaded() { return; } self.client .enqueue_packet(&CSetHealth::new( self.living_entity.health.load(), self.hunger_manager.level.load().into(), self.hunger_manager.saturation.load(), )) .await; } pub async fn tick_health(&self) { if !self.has_client_loaded() { return; } let health = self.living_entity.health.load() as i32; let food = self.hunger_manager.level.load(); let saturation = self.hunger_manager.saturation.load(); let last_health = self.last_sent_health.load(Ordering::Relaxed); let last_food = self.last_sent_food.load(Ordering::Relaxed); let last_saturation = self.last_food_saturation.load(Ordering::Relaxed); if health != last_health || food != last_food || (saturation == 0.0) != last_saturation { self.last_sent_health.store(health, Ordering::Relaxed); self.last_sent_food.store(food, Ordering::Relaxed); self.last_food_saturation .store(saturation == 0.0, Ordering::Relaxed); self.send_health().await; } } pub async fn set_health(&self, health: f32) { self.living_entity.set_health(health).await; self.send_health().await; } pub async fn set_max_health(&self, max_health: f32) { self.living_entity.set_max_health(max_health).await; self.send_health().await; } pub async fn set_food_level(&self, food_level: u8) { self.hunger_manager.set_level(food_level); self.send_health().await; } pub async fn set_saturation(&self, saturation: f32) { self.hunger_manager.set_saturation(saturation); self.send_health().await; } pub fn get_exhaustion(&self) -> f32 { self.hunger_manager.get_exhaustion() } pub async fn set_exhaustion(&self, exhaustion: f32) { self.hunger_manager.set_exhaustion(exhaustion); self.send_health().await; } pub fn get_absorption(&self) -> f32 { self.living_entity.get_absorption() } pub async fn set_absorption(&self, absorption: f32) { self.living_entity.set_absorption(absorption).await; } pub async fn get_ip(&self) -> String { self.client.address().await.to_string() } pub async fn respawn(self: &Arc) { self.world().respawn_player(self, false).await; } pub async fn ban(&self, server: &Server, reason: Option) { let mut banned_players = server.data.banned_player_list.write().await; let string_reason = reason.clone().map_or_else( || "Banned by an operator.".to_string(), pumpkin_util::text::TextComponent::get_text, ); if banned_players .banned_players .iter() .any(|entry| entry.uuid == self.gameprofile.id) { return; } banned_players.banned_players.push( crate::data::banlist_serializer::BannedPlayerEntry::new( &self.gameprofile, "Plugin".to_string(), None, string_reason, ), ); banned_players.save(); drop(banned_players); let kick_reason = reason.unwrap_or_else(|| { TextComponent::translate(translation::MULTIPLAYER_DISCONNECT_BANNED, []) }); self.kick(DisconnectReason::Kicked, kick_reason).await; } pub async fn ban_ip(&self, server: &Server, reason: Option) { let mut banned_ips = server.data.banned_ip_list.write().await; let string_reason = reason.clone().map_or_else( || "Banned by an operator.".to_string(), pumpkin_util::text::TextComponent::get_text, ); let target_ip = self.client.address().await.ip(); if banned_ips.get_entry(&target_ip).is_some() { return; } banned_ips .banned_ips .push(crate::data::banlist_serializer::BannedIpEntry::new( target_ip, "Plugin".to_string(), None, string_reason, )); banned_ips.save(); drop(banned_ips); let kick_reason = reason.unwrap_or_else(|| { TextComponent::translate(translation::MULTIPLAYER_DISCONNECT_IP_BANNED, []) }); let affected = server.get_players_by_ip(target_ip).await; for target in affected { target .kick(DisconnectReason::Kicked, kick_reason.clone()) .await; } } pub fn tick_client_load_timeout(&self) { if !self.client_loaded.load(Ordering::Relaxed) { let timeout = self.client_loaded_timeout.load(Ordering::Relaxed); self.client_loaded_timeout .store(timeout.saturating_sub(1), Ordering::Relaxed); } } async fn handle_killed(&self, death_msg: TextComponent) { self.set_client_loaded(false); let block_pos = self.position().to_block_pos(); let keep_inventory = { self.world().level_info.load().game_rules.keep_inventory }; if !keep_inventory { for item in &self.inventory().main_inventory { let mut lock = item.lock().await; self.world() .drop_stack( &block_pos, mem::replace(&mut *lock, ItemStack::EMPTY.clone()), ) .await; } } // Reset air supply & drowning ticks on death self.breath_manager.reset(self).await; self.client .send_packet_now(&CCombatDeath::new(self.entity_id().into(), &death_msg)) .await; } pub async fn set_gamemode(self: &Arc, gamemode: GameMode) -> bool { // We could send the same gamemode without any problems. But why waste bandwidth? // assert_ne!( // self.gamemode.load(), // gamemode, // "Attempt to set the gamemode to the already current gamemode" // ); // Why are we panicking if the gamemodes are the same? Vanilla just exits early. if self.gamemode.load() == gamemode { return false; } let server = self.world().server.upgrade().unwrap(); send_cancellable! {{ server; PlayerGamemodeChangeEvent { player: self.clone(), new_gamemode: gamemode, previous_gamemode: self.gamemode.load(), cancelled: false, }; 'after: { let gamemode = event.new_gamemode; self.gamemode.store(gamemode); // TODO: Fix this when mojang fixes it // This is intentional to keep the pure vanilla mojang experience // self.previous_gamemode.store(self.previous_gamemode.load()); { // Use another scope so that we instantly unlock `abilities`. let mut abilities = self.abilities.lock().await; abilities.set_for_gamemode(gamemode); }; self.send_abilities_update().await; if gamemode == GameMode::Creative { self.get_entity().extinguish(); self.get_entity().set_on_fire(false).await; } // Stop elytra flight and reset sneaking when switching to spectator mode if gamemode == GameMode::Spectator { let entity = self.get_entity(); if entity.fall_flying.load(Ordering::Relaxed) { entity.set_fall_flying(false).await; } if entity.sneaking.load(Ordering::Relaxed) { entity.set_sneaking(false).await; } } self.living_entity.entity.invulnerable.store( matches!(gamemode, GameMode::Creative | GameMode::Spectator), Ordering::Relaxed, ); self.living_entity .entity .world .load() .broadcast_packet_all(&CPlayerInfoUpdate::new( PlayerInfoFlags::UPDATE_GAME_MODE.bits(), &[pumpkin_protocol::java::client::play::Player { uuid: self.gameprofile.id, actions: &[PlayerAction::UpdateGameMode((gamemode as i32).into())], }], )) .await; self.client .enqueue_packet(&CGameEvent::new( GameEvent::ChangeGameMode, gamemode as i32 as f32, )).await; true } 'cancelled: { false } }} } /// Send the player's skin layers and used hand to all players. pub async fn send_client_information(&self) { let config = self.config.load(); self.living_entity .entity .send_meta_data(&[ Metadata::new( TrackedData::PLAYER_MODE_CUSTOMISATION, MetaDataType::BYTE, config.skin_parts, ), // Metadata::new( // TrackedData::DATA_MAIN_ARM_ID, // MetaDataType::ARM, // VarInt(config.main_hand as u8 as i32), // ), ]) .await; } pub async fn can_harvest(&self, state: &BlockState, block: &'static Block) -> bool { !state.tool_required() || self .inventory .held_item() .lock() .await .is_correct_for_drops(block) } pub async fn get_mining_speed(&self, block: &'static Block) -> f32 { let mut speed = self.inventory.held_item().lock().await.get_speed(block); // Haste if self.living_entity.has_effect(&StatusEffect::HASTE).await || self .living_entity .has_effect(&StatusEffect::CONDUIT_POWER) .await { speed *= ((self.get_haste_amplifier().await + 1) as f32).mul_add(0.2, 1.0); } // Fatigue if let Some(fatigue) = self .living_entity .get_effect(&StatusEffect::MINING_FATIGUE) .await { let fatigue_speed = match fatigue.amplifier { 0 => 0.3, 1 => 0.09, 2 => 0.0027, _ => 8.1E-4, }; speed *= fatigue_speed; } // TODO: Handle when in water if !self.living_entity.entity.on_ground.load(Ordering::Relaxed) { speed /= 5.0; } speed } async fn get_haste_amplifier(&self) -> u32 { let mut i = 0; let mut j = 0; if let Some(effect) = self.living_entity.get_effect(&StatusEffect::HASTE).await { i = effect.amplifier; } if let Some(effect) = self .living_entity .get_effect(&StatusEffect::CONDUIT_POWER) .await { j = effect.amplifier; } u32::from(i.max(j)) } pub async fn send_message( &self, message: &TextComponent, chat_type: u8, sender_name: &TextComponent, target_name: Option<&TextComponent>, ) { self.client .enqueue_packet(&CDisguisedChatMessage::new( message, (chat_type + 1).into(), sender_name, target_name, )) .await; } /// Sends a custom payload packet to this player (Java edition only). pub async fn send_custom_payload(&self, channel: &str, data: &[u8]) { if let ClientPlatform::Java(java) = &self.client { java.enqueue_packet(&CCustomPayload::new(channel, data)) .await; } } pub async fn drop_item(&self, item_stack: ItemStack) { let item_pos = self.living_entity.entity.pos.load() + Vector3::new(0.0, self.living_entity.entity.get_eye_height() - 0.3, 0.0); let entity = Entity::new(self.world(), item_pos, &EntityType::ITEM); let pitch = f64::from(self.living_entity.entity.pitch.load()).to_radians(); let yaw = f64::from(self.living_entity.entity.yaw.load()).to_radians(); let pitch_sin = pitch.sin(); let pitch_cos = pitch.cos(); let yaw_sin = yaw.sin(); let yaw_cos = yaw.cos(); let horizontal_offset = rand::random::() * TAU; let l = 0.02 * rand::random::(); let velocity = Vector3::new( (-yaw_sin * pitch_cos).mul_add(0.3, horizontal_offset.cos() * l), (rand::random::() - rand::random::()) .mul_add(0.1, (-pitch_sin).mul_add(0.3, 0.1)), (yaw_cos * pitch_cos).mul_add(0.3, horizontal_offset.sin() * l), ); // TODO: Merge stacks together let item_entity = Arc::new(ItemEntity::new_with_velocity(entity, item_stack, velocity, 40).await); self.world().spawn_entity(item_entity).await; } pub async fn drop_held_item(&self, drop_stack: bool) { // Do not hold both item stack and screen handler locks at the same time. let (dropped_stack, updated_stack, selected_slot) = { let binding = self.inventory.held_item(); let mut item_stack = binding.lock().await; if item_stack.is_empty() { return; } let drop_amount = if drop_stack { item_stack.item_count } else { 1 }; let dropped_stack = item_stack.copy_with_count(drop_amount); item_stack.decrement(drop_amount); let updated_stack = item_stack.clone(); let selected_slot = self.inventory.get_selected_slot(); (dropped_stack, updated_stack, selected_slot) }; self.drop_item(dropped_stack).await; let inv: Arc = self.inventory.clone(); let screen_binding = self.current_screen_handler.lock().await; let mut screen_handler = screen_binding.lock().await; if let Some(slot_index) = screen_handler .get_slot_index(&inv, selected_slot as usize) .await { screen_handler.set_received_stack(slot_index, updated_stack); } } pub async fn swap_item(&self) { let (main_hand_item, off_hand_item) = self.inventory.swap_item().await; let equipment = &[ (EquipmentSlot::MAIN_HAND, main_hand_item), (EquipmentSlot::OFF_HAND, off_hand_item), ]; self.living_entity.send_equipment_changes(equipment).await; // todo this.player.stopUsingItem(); } pub async fn send_system_message(&self, text: &TextComponent) { self.send_system_message_raw(text, false).await; } pub async fn send_system_message_raw(&self, text: &TextComponent, overlay: bool) { match &self.client { ClientPlatform::Java(client) => { client .enqueue_packet(&CSystemChatMessage::new(text, overlay)) .await; } ClientPlatform::Bedrock(client) => { client .send_game_packet(&SText::system_message(text.clone().get_text())) .await; } } } pub async fn tick_experience(&self) { if !self.has_client_loaded() { return; } let level = self.experience_level.load(Ordering::Relaxed); if self.last_sent_xp.load(Ordering::Relaxed) != level { let progress = self.experience_progress.load(); let points = self.experience_points.load(Ordering::Relaxed); self.last_sent_xp.store(level, Ordering::Relaxed); self.client .send_packet_now(&CSetExperience::new( progress.clamp(0.0, 1.0), level.into(), points.into(), )) .await; } } /// Sets the player's experience level and notifies the client. pub async fn set_experience(&self, level: i32, progress: f32, points: i32) { // TODO: These should be atomic together, not isolated; make a struct containing these. can cause ABA issues self.experience_level.store(level, Ordering::Relaxed); self.experience_progress.store(progress.clamp(0.0, 1.0)); self.experience_points.store(points, Ordering::Relaxed); self.last_sent_xp.store(-1, Ordering::Relaxed); self.tick_experience().await; if self.has_client_loaded() { self.client .enqueue_packet(&CSetExperience::new( progress.clamp(0.0, 1.0), level.into(), points.into(), )) .await; } } /// Sets the player's experience level directly. pub async fn set_experience_level(&self, new_level: i32, keep_progress: bool) { let progress = self.experience_progress.load(); let mut points = self.experience_points.load(Ordering::Relaxed); // If `keep_progress` is `true` then calculate the number of points needed to keep the same progress scaled. if keep_progress { // Get our current level let current_level = self.experience_level.load(Ordering::Relaxed); let current_max_points = experience::points_in_level(current_level); // Calculate the max value for the new level let new_max_points = experience::points_in_level(new_level); // Calculate the scaling factor let scale = new_max_points as f32 / current_max_points as f32; // Scale the points (Vanilla doesn't seem to recalculate progress so we won't) points = (points as f32 * scale) as i32; } self.set_experience(new_level, progress, points).await; } pub async fn add_effect(&self, effect: Effect) { self.living_entity.add_effect(effect).await; } pub async fn send_active_effects(&self) { let effects = self.living_entity.active_effects.lock().await; for effect in effects.values() { self.send_effect(effect.clone()).await; } } /** * Send a clientside only effect to the player. * It won't be tracked on the server. */ pub async fn send_effect(&self, effect: Effect) { let mut flag: i8 = 0; if effect.ambient { flag |= 1; } if effect.show_particles { flag |= 2; } if effect.show_icon { flag |= 4; } if effect.blend { flag |= 8; } let effect_id = VarInt(i32::from(effect.effect_type.id)); self.client .enqueue_packet(&CUpdateMobEffect::new( self.entity_id().into(), effect_id, effect.amplifier.into(), effect.duration.into(), flag, )) .await; } pub async fn remove_effect(&self, effect_type: &'static StatusEffect) -> bool { let effect_id = VarInt(i32::from(effect_type.id)); self.client .enqueue_packet( &pumpkin_protocol::java::client::play::CRemoveMobEffect::new( self.entity_id().into(), effect_id, ), ) .await; self.living_entity.remove_effect(effect_type).await // TODO broadcast metadata } pub async fn remove_all_effects(&self) -> bool { let mut succeeded = false; let mut effect_list = vec![]; for effect in self.living_entity.active_effects.lock().await.keys() { effect_list.push(*effect); let effect_id = VarInt(i32::from(effect.id)); self.client .enqueue_packet( &pumpkin_protocol::java::client::play::CRemoveMobEffect::new( self.entity_id().into(), effect_id, ), ) .await; succeeded = true; } // Need to remove effects afterward here because there would be a deadlock if this is done in the for loop. for effect in effect_list { self.living_entity.remove_effect(effect).await; } succeeded } /// Add experience levels to the player. pub async fn add_experience_levels(&self, added_levels: i32) { let current_level = self.experience_level.load(Ordering::Relaxed); let new_level = current_level + added_levels; self.set_experience_level(new_level, true).await; } /// Set the player's experience points directly. Returns `true` if successful. pub async fn set_experience_points(&self, new_points: i32) -> bool { let current_points = self.experience_points.load(Ordering::Relaxed); if new_points == current_points { return true; } let current_level = self.experience_level.load(Ordering::Relaxed); let max_points = experience::points_in_level(current_level); if new_points < 0 || new_points > max_points { return false; } let progress = new_points as f32 / max_points as f32; self.set_experience(current_level, progress, new_points) .await; true } /// Add experience points to the player. pub async fn add_experience_points(self: &Arc, mut added_points: i32) { if let Some(server) = self.world().server.upgrade() { let event = PlayerExpChangeEvent::new(self.clone(), added_points); let event = server.plugin_manager.fire(event).await; added_points = event.amount; } let current_level = self.experience_level.load(Ordering::Relaxed); let current_points = self.experience_points.load(Ordering::Relaxed); let total_exp = experience::points_to_level(current_level) + current_points; let new_total_exp = total_exp + added_points; let (new_level, new_points) = experience::total_to_level_and_points(new_total_exp); let progress = experience::progress_in_level(new_points, new_level); self.set_experience(new_level, progress, new_points).await; } pub async fn apply_mending_from_xp(&self, mut xp: i32) -> i32 { if xp <= 0 { return xp; } let mut candidates: Vec<(usize, EquipmentSlot, Arc>)> = Vec::new(); let selected_slot = self.inventory.get_selected_slot() as usize; let mut slot_pairs: Vec<(usize, EquipmentSlot)> = vec![ (selected_slot, EquipmentSlot::MAIN_HAND), (PlayerInventory::OFF_HAND_SLOT, EquipmentSlot::OFF_HAND), ]; for (slot_index, slot) in self.inventory.equipment_slots.iter() { if slot.is_armor_slot() { slot_pairs.push((*slot_index, slot.clone())); } } for (slot_index, equipment_slot) in slot_pairs { let stack = self.inventory.get_stack(slot_index).await; let eligible = { let s = stack.lock().await; s.get_enchantment_level(&Enchantment::MENDING) > 0 && s.get_damage() > 0 }; if eligible { candidates.push((slot_index, equipment_slot, stack)); } } if candidates.is_empty() { return xp; } let idx = rand::random::() as usize % candidates.len(); let (slot_index, equipment_slot, stack) = candidates.swap_remove(idx); let (updated_stack, repaired) = { let mut stack = stack.lock().await; let repaired = stack.repair_item(xp.saturating_mul(2)); (stack.clone(), repaired) }; if repaired <= 0 { return xp; } let xp_used = (repaired + 1) / 2; xp = xp.saturating_sub(xp_used); self.enqueue_slot_set_packet(&CSetPlayerInventory::new( (slot_index as i32).into(), &ItemStackSerializer::from(updated_stack.clone()), )) .await; self.living_entity .send_equipment_changes(&[(equipment_slot, updated_stack)]) .await; xp } pub fn increment_screen_handler_sync_id(&self) { let current_id = self.screen_handler_sync_id.load(Ordering::Relaxed); self.screen_handler_sync_id .store(current_id % 100 + 1, Ordering::Relaxed); } pub async fn close_handled_screen(&self) { self.client .enqueue_packet(&CCloseContainer::new( self.current_screen_handler .lock() .await .lock() .await .sync_id() .into(), )) .await; self.on_handled_screen_closed().await; } pub async fn on_handled_screen_closed(&self) { // let window_type = { // let handler_lock = self.current_screen_handler.lock().await; // let mut handler = handler_lock.lock().await; // let wt = handler.window_type(); // handler.on_closed(self).await; // wt // }; // TODO // if let Some(server) = self.living_entity.entity.world.load().server.upgrade() { // server // .plugin_manager // .fire(InventoryCloseEvent::new(&self, window_type)) // .await; // } let player_screen_handler: Arc> = self.player_screen_handler.clone(); let current_screen_handler: Arc> = self.current_screen_handler.lock().await.clone(); if !Arc::ptr_eq(&player_screen_handler, ¤t_screen_handler) { player_screen_handler .lock() .await .copy_shared_slots(current_screen_handler) .await; } *self.current_screen_handler.lock().await = self.player_screen_handler.clone(); self.open_container_pos.store(None); } pub async fn on_screen_handler_opened(&self, screen_handler: Arc>) { let mut screen_handler = screen_handler.lock().await; screen_handler .add_listener(self.screen_handler_listener.clone()) .await; screen_handler .update_sync_handler(self.screen_handler_sync_handler.clone()) .await; } pub async fn open_handled_screen( &self, screen_handler_factory: &dyn ScreenHandlerFactory, block_pos: Option, ) -> Option { if !self .current_screen_handler .lock() .await .lock() .await .as_any() .is::() { self.close_handled_screen().await; } self.increment_screen_handler_sync_id(); if let Some(screen_handler) = screen_handler_factory .create_screen_handler( self.screen_handler_sync_id.load(Ordering::Relaxed), &self.inventory, self, ) .await { let screen_handler_temp = screen_handler.lock().await; self.client .enqueue_packet(&COpenScreen::new( screen_handler_temp.sync_id().into(), (screen_handler_temp .window_type() .expect("Can't open PlayerScreenHandler") as i32) .into(), &screen_handler_factory.get_display_name(), )) .await; drop(screen_handler_temp); self.on_screen_handler_opened(screen_handler.clone()).await; *self.current_screen_handler.lock().await = screen_handler; self.open_container_pos.store(block_pos); Some(self.screen_handler_sync_id.load(Ordering::Relaxed)) } else { //TODO: Send message if spectator None } } pub async fn open_handled_screen_direct( &self, screen_handler: Arc>, title: TextComponent, ) { if !self .current_screen_handler .lock() .await .lock() .await .as_any() .is::() { self.close_handled_screen().await; } let screen_handler_temp = screen_handler.lock().await; self.client .enqueue_packet(&COpenScreen::new( screen_handler_temp.sync_id().into(), (screen_handler_temp .window_type() .expect("Can't open PlayerScreenHandler") as i32) .into(), &title, )) .await; drop(screen_handler_temp); self.on_screen_handler_opened(screen_handler.clone()).await; *self.current_screen_handler.lock().await = screen_handler; self.open_container_pos.store(None); } #[allow(clippy::too_many_lines)] pub async fn on_slot_click(self: &Arc, packet: SClickSlot, server: &Server) { self.update_last_action_time(); let screen_handler_arc = self.current_screen_handler.lock().await.clone(); let mut screen_handler = screen_handler_arc.lock().await; let (sync_id, container_slots, allow_grab_items, allow_put_items) = { let b = screen_handler.get_behaviour(); ( b.sync_id, b.container_slots, b.allow_grab_items, b.allow_put_items, ) }; if i32::from(sync_id) != packet.sync_id.0 { return; } if self.gamemode.load() == GameMode::Spectator { screen_handler.sync_state().await; return; } if !screen_handler.can_use(self.as_ref()) { warn!( "Player {} interacted with invalid menu {:?}", self.gameprofile.name, screen_handler.window_type() ); return; } let slot = packet.slot; if !screen_handler.is_slot_valid(i32::from(slot)).await { warn!( "Player {} clicked invalid slot index: {}, available slots: {}", self.gameprofile.name, slot, screen_handler.get_behaviour().slots.len() ); return; } // Fire InventoryClickEvent let clicked_item = if slot >= 0 { let slot_obj = &screen_handler.get_behaviour().slots[slot as usize]; Some(slot_obj.get_cloned_stack().await) } else { None }; let cursor_item = Some( screen_handler .get_behaviour() .cursor_stack .lock() .await .clone(), ); let raw_slot = slot; // For now raw_slot == slot, as we don't have separate view/inventory indexing yet let hotbar_button = if matches!(packet.mode, SlotActionType::Swap) { packet.button } else { -1 }; let click_type = match packet.mode { SlotActionType::Pickup => { if packet.button == 0 { ClickType::Left } else { ClickType::Right } } SlotActionType::QuickMove => { if packet.button == 0 { ClickType::ShiftLeft } else { ClickType::ShiftRight } } SlotActionType::Swap => ClickType::NumberKey(packet.button as u8), SlotActionType::Clone => ClickType::Middle, SlotActionType::Throw => { if packet.button == 0 { ClickType::Drop } else { ClickType::ControlDrop } } SlotActionType::QuickCraft => { if [0, 4, 8].contains(&packet.button) { ClickType::Left } else if [1, 5, 9].contains(&packet.button) { ClickType::Right } else { ClickType::Middle } } SlotActionType::PickupAll => ClickType::DoubleClick, }; send_cancellable! {{ server; InventoryClickEvent::new( self, screen_handler.window_type(), click_type, slot, raw_slot, clicked_item, cursor_item, i32::from(hotbar_button), ); 'after: {} 'cancelled: { screen_handler.cancel().await; return; } }} // Enforce flags let is_container_slot = slot >= 0 && i32::from(slot) < container_slots as i32; match packet.mode { SlotActionType::Pickup => { let cursor_stack = screen_handler.get_behaviour().cursor_stack.lock().await; if is_container_slot { if !cursor_stack.is_empty() && !allow_put_items { drop(cursor_stack); screen_handler.cancel().await; return; } if cursor_stack.is_empty() && !allow_grab_items { drop(cursor_stack); screen_handler.cancel().await; return; } } } SlotActionType::QuickMove => { if is_container_slot && !allow_grab_items { screen_handler.cancel().await; return; } if !is_container_slot && !allow_put_items { screen_handler.cancel().await; return; } } SlotActionType::Swap => { if is_container_slot && (!allow_grab_items || !allow_put_items) { screen_handler.cancel().await; return; } } SlotActionType::Throw => { if is_container_slot && !allow_grab_items { screen_handler.cancel().await; return; } } SlotActionType::QuickCraft => { if !allow_put_items { // Dragging items into slots screen_handler.cancel().await; return; } } SlotActionType::PickupAll => { if !allow_grab_items { screen_handler.cancel().await; return; } } SlotActionType::Clone => {} } let not_in_sync = packet.revision.0 != (screen_handler .get_behaviour() .revision .load(Ordering::Relaxed) as i32); screen_handler.disable_sync(); screen_handler .on_slot_click( i32::from(slot), i32::from(packet.button), packet.mode.clone(), self.as_ref(), ) .await; for (key, value) in packet.array_of_changed_slots { screen_handler.set_received_hash(key as usize, value); } screen_handler.set_received_cursor_hash(packet.carried_item); screen_handler.enable_sync(); if not_in_sync { screen_handler.update_to_client().await; } else { screen_handler.send_content_updates().await; } } /// Check if the player has a specific permission pub async fn has_permission(self: &Arc, server: &Server, node: &str) -> bool { let perm_manager = server.permission_manager.read().await; let result = perm_manager .has_permission(&self.gameprofile.id, node, self.permission_lvl.load()) .await; drop(perm_manager); let event = server .plugin_manager .fire(PlayerPermissionCheckEvent::new( self.clone(), node.to_string(), result, )) .await; event.result } pub fn is_creative(&self) -> bool { self.gamemode.load() == GameMode::Creative } /// Swing the hand of the player pub async fn swing_hand(&self, hand: Hand, all: bool) { let world = self.world(); let entity_id = VarInt(self.entity_id()); let chunk_pos = self.living_entity.entity.chunk_pos.load(); let animation = match hand { Hand::Left => Animation::SwingMainArm, Hand::Right => Animation::SwingOffhand, }; let packet = CEntityAnimation::new(entity_id, animation); if all { world.broadcast_to_chunk(chunk_pos, &packet).await; } else { world .broadcast_to_chunk_except(chunk_pos, &[self.get_entity().entity_uuid], &packet) .await; } } /// Start using an item (e.g. drawing a bow) pub fn start_using_item(&self, hand: Hand) { self.using_item.store(true, Ordering::Relaxed); self.item_use_start_time .store(self.tick_counter.load(Ordering::Relaxed), Ordering::Relaxed); self.using_hand.store(Some(hand)); } /// Stop using an item pub fn stop_using_item(&self) { self.using_item.store(false, Ordering::Relaxed); self.using_hand.store(None); } /// Get the number of ticks the item has been in use pub fn get_item_use_ticks(&self) -> i32 { if !self.using_item.load(Ordering::Relaxed) { return 0; } self.tick_counter.load(Ordering::Relaxed) - self.item_use_start_time.load(Ordering::Relaxed) } /// Find arrow in inventory (main hand, offhand, or inventory slots) pub async fn find_arrow(&self) -> Option { use pumpkin_data::item::Item; let inventory = self.inventory(); // Check offhand first let stack = inventory.get_stack(PlayerInventory::OFF_HAND_SLOT).await; let item = stack.lock().await; if item.item.id == Item::ARROW.id && item.item_count > 0 { return Some(PlayerInventory::OFF_HAND_SLOT); } drop(item); // Check hotbar and main inventory for slot in 0..PlayerInventory::MAIN_SIZE { let stack = inventory.get_stack(slot).await; let item = stack.lock().await; if item.item.id == Item::ARROW.id && item.item_count > 0 { return Some(slot); } } None } /// Consume one arrow from the specified slot pub async fn consume_arrow(&self, slot: usize) -> bool { let gamemode = self.gamemode.load(); if gamemode == GameMode::Creative { return true; // Don't consume in creative } let inventory = self.inventory(); let stack_arc = inventory.get_stack(slot).await; let mut stack = stack_arc.lock().await; match stack.item_count { 2.. => { stack.item_count -= 1; true } 1 => { *stack = ItemStack::EMPTY.clone(); true } _ => false, } } /// Returns the main non-air `BlockPos` underneath the player. pub async fn get_supporting_block_pos(&self) -> Option { let entity = self.get_entity(); let entity_pos = entity.pos.load(); let aabb = entity.bounding_box.load(); let world = self.world(); // Create the thin bounding box directly underneath the entity's feet let footprint = BoundingBox::new( Vector3::new(aabb.min.x, aabb.min.y - 1.0e-6, aabb.min.z), Vector3::new(aabb.max.x, aabb.min.y, aabb.max.z), ); let min_pos = footprint.min_block_pos(); let max_pos = footprint.max_block_pos(); let mut closest_candidate = None; let mut min_dist_sq = f64::MAX; // Iterate through candidates for pos in BlockPos::iterate(min_pos, max_pos) { let (_, state) = world.get_block_and_state(&pos).await; // Only consider physical blocks if state.is_air() { continue; } // Calculate distance squared from the block's center to the entity's position let block_center_x = f64::from(pos.0.x) + 0.5; let block_center_y = f64::from(pos.0.y) + 0.5; let block_center_z = f64::from(pos.0.z) + 0.5; let dx = block_center_x - entity_pos.x; let dy = block_center_y - entity_pos.y; let dz = block_center_z - entity_pos.z; let dist_sq = dx * dx + dy * dy + dz * dz; // Pick the block with the smallest distance if dist_sq < min_dist_sq { min_dist_sq = dist_sq; closest_candidate = Some(pos); } else if (dist_sq - min_dist_sq).abs() < f64::EPSILON { // If the distance is the same, pick the block with the smallest y, then z, then x if let Some(best_pos) = closest_candidate { let is_smaller = pos.0.y < best_pos.0.y || (pos.0.y == best_pos.0.y && pos.0.z < best_pos.0.z) || (pos.0.y == best_pos.0.y && pos.0.z == best_pos.0.z && pos.0.x < best_pos.0.x); if is_smaller { closest_candidate = Some(pos); } } } } // Return the closest block if we found one if closest_candidate.is_some() { return closest_candidate; } // Fallback to the block directly underneath the player's position if no candidates were found let fallback_pos = BlockPos::new( entity_pos.x.floor() as i32, (entity_pos.y - 0.2).floor() as i32, entity_pos.z.floor() as i32, ); let state = world.get_block_state(&fallback_pos).await; (!state.is_air()).then_some(fallback_pos) } pub async fn get_command_source(self: &Arc, server: &Arc) -> CommandSource { CommandSender::Player(self.clone()) .into_source(server) .await } } impl PartialEq for Player { fn eq(&self, other: &Self) -> bool { self.gameprofile.id == other.gameprofile.id } } impl NBTStorage for Player { fn write_nbt<'a>(&'a self, nbt: &'a mut PNbtCompound) -> NbtFuture<'a, ()> { Box::pin(async move { nbt.put_int(DATA_VERSION); nbt.put_string(self.world().dimension.minecraft_name); self.living_entity.write_nbt(nbt).await; self.inventory.write_nbt(nbt).await; self.ender_chest_inventory.write_nbt(nbt).await; self.abilities.lock().await.write_nbt(nbt).await; let total_exp = experience::points_to_level(self.experience_level.load(Ordering::Relaxed)) + self.experience_points.load(Ordering::Relaxed); nbt.put_int(total_exp); nbt.put_byte(self.gamemode.load() as i8); if let Some(previous_gamemode) = self.previous_gamemode.load() { nbt.put_bool(true); nbt.put_byte(previous_gamemode as i8); } else { nbt.put_bool(false); } nbt.put_bool(self.has_played_before.load(Ordering::Relaxed)); self.hunger_manager.write_nbt(nbt).await; if let Some(respawn) = self.respawn_point.load().as_ref() { nbt.put_bool(true); nbt.put_int(respawn.position.0.x); nbt.put_int(respawn.position.0.y); nbt.put_int(respawn.position.0.z); nbt.put_string(respawn.dimension.minecraft_name); nbt.put_bool(respawn.force); } else { nbt.put_bool(false); } }) } fn read_nbt<'a>(&'a mut self, nbt: &'a mut PNbtCompound) -> NbtFuture<'a, ()> { Box::pin(async move { let _version = nbt.get_int().unwrap_or(0); let _dimension_name = nbt.get_string().unwrap_or_default(); self.living_entity.read_nbt(nbt).await; self.inventory.read_nbt_non_mut(nbt).await; self.ender_chest_inventory.read_nbt_non_mut(nbt).await; self.abilities.lock().await.read_nbt(nbt).await; let total_exp = nbt.get_int().unwrap_or(0); let (level, points) = experience::total_to_level_and_points(total_exp); let progress = experience::progress_in_level(level, points); self.experience_level.store(level, Ordering::Relaxed); self.experience_progress.store(progress); self.experience_points.store(points, Ordering::Relaxed); self.gamemode.store( GameMode::try_from(nbt.get_byte().unwrap_or(0)).unwrap_or(GameMode::Survival), ); if nbt.get_bool().unwrap_or(false) { self.previous_gamemode .store(GameMode::try_from(nbt.get_byte().unwrap_or(0)).ok()); } self.has_played_before .store(nbt.get_bool().unwrap_or(false), Ordering::Relaxed); self.hunger_manager.read_nbt(nbt).await; if nbt.get_bool().unwrap_or(false) { let x = nbt.get_int().unwrap_or(0); let y = nbt.get_int().unwrap_or(0); let z = nbt.get_int().unwrap_or(0); let dim = nbt .get_string() .ok() .and_then(|s| Dimension::from_name(s.as_str()).copied()) .unwrap_or(self.world().dimension); let force = nbt.get_bool().unwrap_or(false); self.respawn_point.store(Some(RespawnPoint { dimension: dim, position: BlockPos(Vector3::new(x, y, z)), yaw: 0.0, force, })); } }) } } impl NBTStorageInit for Player {} impl NBTStorage for PlayerInventory { fn write_nbt<'a>(&'a self, nbt: &'a mut PNbtCompound) -> NbtFuture<'a, ()> { Box::pin(async move { // Save the selected slot (hotbar) nbt.put_u8(self.get_selected_slot()); // Save items count then items let mut present_items = Vec::new(); for (i, item) in self.main_inventory.iter().enumerate() { let stack = item.lock().await; if !stack.is_empty() { present_items.push((i as u8, stack.clone())); } } nbt.put_u32(present_items.len() as u32); for (slot, stack) in present_items { nbt.put_u8(slot); stack.write_item_stack_pnbt(nbt); } // Save equipment #[allow(clippy::default_trait_access)] for slot in [ EquipmentSlot::OffHand(Default::default()), EquipmentSlot::Head(Default::default()), EquipmentSlot::Chest(Default::default()), EquipmentSlot::Legs(Default::default()), EquipmentSlot::Feet(Default::default()), ] { // Find the actual slot in self.equipment_slots let mut found = false; for s in self.equipment_slots.values() { if mem::discriminant(s) == mem::discriminant(&slot) { let stack_binding = self.entity_equipment.lock().await.get(s); let stack = stack_binding.lock().await; if !stack.is_empty() { nbt.put_bool(true); stack.write_item_stack_pnbt(nbt); found = true; } break; } } if !found { nbt.put_bool(false); } } }) } fn read_nbt_non_mut<'a>(&'a self, nbt: &'a mut PNbtCompound) -> NbtFuture<'a, ()> { Box::pin(async move { // Read selected hotbar slot self.set_selected_slot(nbt.get_u8().unwrap_or(0)); // Process inventory list let items_len = nbt.get_u32().unwrap_or(0) as usize; for _ in 0..items_len { let slot = nbt.get_u8().unwrap_or(0) as usize; if let Some(item_stack) = ItemStack::read_item_stack_pnbt(nbt) { self.set_stack(slot, item_stack).await; } } // Read equipment for slot_idx in [40, 39, 38, 37, 36] { // offhand, head, chest, legs, feet if nbt.get_bool().unwrap_or(false) && let Some(item_stack) = ItemStack::read_item_stack_pnbt(nbt) { self.set_stack(slot_idx, item_stack).await; } } }) } } impl NBTStorageInit for PlayerInventory {} impl NBTStorage for EnderChestInventory { fn write_nbt<'a>(&'a self, nbt: &'a mut PNbtCompound) -> NbtFuture<'a, ()> { Box::pin(async { // Save items count then items let mut present_items = Vec::new(); for (i, item) in self.items.iter().enumerate() { let stack = item.lock().await; if !stack.is_empty() { present_items.push((i as u8, stack.clone())); } } nbt.put_u32(present_items.len() as u32); for (slot, stack) in present_items { nbt.put_u8(slot); stack.write_item_stack_pnbt(nbt); } }) } fn read_nbt_non_mut<'a>(&'a self, nbt: &'a mut PNbtCompound) -> NbtFuture<'a, ()> { Box::pin(async { // Process item list let items_len = nbt.get_u32().unwrap_or(0) as usize; for _ in 0..items_len { let slot = nbt.get_u8().unwrap_or(0) as usize; if let Some(item_stack) = ItemStack::read_item_stack_pnbt(nbt) { self.set_stack(slot, item_stack).await; } } }) } } impl NBTStorageInit for EnderChestInventory {} impl EntityBase for Player { fn damage_with_context<'a>( &'a self, caller: &'a dyn EntityBase, amount: f32, damage_type: DamageType, position: Option>, source: Option<&'a dyn EntityBase>, cause: Option<&'a dyn EntityBase>, ) -> EntityBaseFuture<'a, bool> { Box::pin(async move { if self.abilities.lock().await.invulnerable && damage_type != DamageType::GENERIC_KILL && damage_type != DamageType::OUT_OF_WORLD { return false; } // TODO: Implement shield blocking durability. let result = self .living_entity .damage_with_context(caller, amount, damage_type, position, source, cause) .await; if result { let health = self.living_entity.health.load(); if health <= 0.0 { let death_message = LivingEntity::get_death_message(caller, damage_type, source, cause).await; self.handle_killed(death_message).await; } } result }) } fn teleport( self: Arc, position: Vector3, yaw: Option, pitch: Option, world: Arc, ) -> TeleportFuture { Box::pin(async move { if Arc::ptr_eq(&world, &self.world()) { // Same world let yaw = yaw.unwrap_or(self.living_entity.entity.yaw.load()); let pitch = pitch.unwrap_or(self.living_entity.entity.pitch.load()); let server = self.world().server.upgrade().unwrap(); send_cancellable! {{ server; PlayerTeleportEvent { player: self.clone(), from: self.living_entity.entity.pos.load(), to: position, cancelled: false, }; 'after: { let position = event.to; let entity = self.get_entity(); self.request_teleport(position, yaw, pitch).await; let chunk_pos = entity.chunk_pos.load(); entity .world .load() .broadcast_to_chunk_except( chunk_pos, &[self.living_entity.entity.entity_uuid], &CEntityPositionSync::new( self.living_entity.entity.entity_id.into(), position, Vector3::new(0.0, 0.0, 0.0), yaw, pitch, entity.on_ground.load(Ordering::SeqCst), ) ) .await; } }} } else { self.teleport_world(world, position, yaw, pitch).await; } }) } fn get_entity(&self) -> &Entity { &self.living_entity.entity } fn get_living_entity(&self) -> Option<&LivingEntity> { Some(&self.living_entity) } fn get_player(&self) -> Option<&Player> { Some(self) } fn is_spectator(&self) -> bool { self.gamemode.load() == GameMode::Spectator } fn get_name(&self) -> TextComponent { //TODO: team color TextComponent::text(self.gameprofile.name.clone()) } fn get_display_name(&self) -> EntityBaseFuture<'_, TextComponent> { Box::pin(async move { if let Some(display_name) = self.display_name.lock().await.as_ref() { return display_name.clone(); } let name = self.get_name(); let name_clone = name.clone(); let mut name = name.click_event(ClickEvent::SuggestCommand { command: format!("/tell {} ", self.gameprofile.name.clone()).into(), }); name = name.hover_event(HoverEvent::show_entity( self.living_entity.entity.entity_uuid.to_string(), self.living_entity.entity.entity_type.resource_name.into(), Some(name_clone), )); name.insertion(self.gameprofile.name.clone()) }) } fn cast_any(&self) -> &dyn std::any::Any { self } fn as_nbt_storage(&self) -> &dyn NBTStorage { self } fn tick_in_void<'a>(&'a self, dyn_self: &'a dyn EntityBase) -> EntityBaseFuture<'a, ()> { Box::pin(async move { self.living_entity.tick_in_void(dyn_self).await; }) } } #[derive(Debug)] pub enum TitleMode { Title, SubTitle, ActionBar, } /// Represents a player's abilities and special powers. /// /// This struct contains information about the player's current abilities, such as flight, invulnerability, and creative mode. pub struct Abilities { /// Indicates whether the player is invulnerable to damage. pub invulnerable: bool, /// Indicates whether the player is currently flying. pub flying: bool, /// Indicates whether the player is allowed to fly (if enabled). pub allow_flying: bool, /// Indicates whether the player is in creative mode. pub creative: bool, /// Indicates whether the player is allowed to modify the world. pub allow_modify_world: bool, /// The player's flying speed. pub fly_speed: f32, /// The field of view adjustment when the player is walking or sprinting. pub walk_speed: f32, } impl NBTStorage for Abilities { fn write_nbt<'a>(&'a self, nbt: &'a mut PNbtCompound) -> NbtFuture<'a, ()> { Box::pin(async { nbt.put_bool(self.invulnerable); nbt.put_bool(self.flying); nbt.put_bool(self.allow_flying); nbt.put_bool(self.creative); nbt.put_bool(self.allow_modify_world); nbt.put_float(self.fly_speed); nbt.put_float(self.walk_speed); }) } fn read_nbt<'a>(&'a mut self, nbt: &'a mut PNbtCompound) -> NbtFuture<'a, ()> { Box::pin(async move { self.invulnerable = nbt.get_bool().unwrap_or(false); self.flying = nbt.get_bool().unwrap_or(false); self.allow_flying = nbt.get_bool().unwrap_or(false); self.creative = nbt.get_bool().unwrap_or(false); self.allow_modify_world = nbt.get_bool().unwrap_or(false); self.fly_speed = nbt.get_float().unwrap_or(0.05); self.walk_speed = nbt.get_float().unwrap_or(0.1); }) } } impl NBTStorageInit for Abilities {} impl Default for Abilities { fn default() -> Self { Self { invulnerable: false, flying: false, allow_flying: false, creative: false, allow_modify_world: true, fly_speed: 0.05, walk_speed: 0.1, } } } impl Abilities { pub const fn set_for_gamemode(&mut self, gamemode: GameMode) { match gamemode { GameMode::Creative => { // self.flying = false; // Start not flying self.allow_flying = true; self.creative = true; self.invulnerable = true; } GameMode::Spectator => { self.flying = true; self.allow_flying = true; self.creative = false; self.invulnerable = true; } _ => { self.flying = false; self.allow_flying = false; self.creative = false; self.invulnerable = false; } } } } /// Represents the player's stored respawn point (bed/anchor/forced). #[derive(Copy, Debug, Clone, PartialEq)] pub struct RespawnPoint { pub dimension: Dimension, pub position: BlockPos, pub yaw: f32, pub force: bool, } /// Calculated respawn position ready for use. /// Returned by `calculate_respawn_point()`. #[derive(Debug, Clone)] pub struct CalculatedRespawnPoint { /// The exact position to spawn at (centered in block). pub position: Vector3, /// The yaw rotation. pub yaw: f32, /// The pitch rotation. pub pitch: f32, /// The dimension to spawn in. pub dimension: Dimension, } /// Represents the player's chat mode settings. #[derive(Debug, Clone)] pub enum ChatMode { /// Chat is enabled for the player. Enabled, /// The player should only see chat messages from commands. CommandsOnly, /// All messages should be hidden. Hidden, } pub struct InvalidChatMode; impl TryFrom for ChatMode { type Error = InvalidChatMode; fn try_from(value: i32) -> Result { match value { 0 => Ok(Self::Enabled), 1 => Ok(Self::CommandsOnly), 2 => Ok(Self::Hidden), _ => Err(InvalidChatMode), } } } /// Player's current chat session pub struct ChatSession { pub session_id: uuid::Uuid, pub expires_at: i64, pub public_key: Box<[u8]>, pub signature: Box<[u8]>, pub messages_sent: i32, pub messages_received: i32, pub signature_cache: Vec>, } impl Default for ChatSession { fn default() -> Self { Self::new(Uuid::nil(), 0, Box::new([]), Box::new([])) } } impl ChatSession { #[must_use] pub const fn new( session_id: Uuid, expires_at: i64, public_key: Box<[u8]>, key_signature: Box<[u8]>, ) -> Self { Self { session_id, expires_at, public_key, signature: key_signature, messages_sent: 0, messages_received: 0, signature_cache: Vec::new(), } } } #[derive(Clone, Default)] pub struct LastSeen(Vec>); impl From for Vec> { fn from(seen: LastSeen) -> Self { seen.0 } } impl AsRef<[Box<[u8]>]> for LastSeen { fn as_ref(&self) -> &[Box<[u8]>] { &self.0 } } impl LastSeen { /// The sender's `last_seen` signatures are sent as ID's if the recipient has them in their cache. /// Otherwise, the full signature is sent. (ID:0 indicates full signature is being sent) pub async fn indexed_for(&self, recipient: &Arc) -> Box<[PreviousMessage]> { let mut indexed = Vec::new(); for signature in &self.0 { let index = recipient .signature_cache .lock() .await .full_cache .iter() .position(|s| s == signature); if let Some(index) = index { indexed.push(PreviousMessage { // Send ID reference to recipient's cache (index + 1 because 0 is reserved for full signature) id: VarInt(1 + index as i32), signature: None, }); } else { indexed.push(PreviousMessage { // Send ID as 0 for full signature id: VarInt(0), signature: Some(signature.clone()), }); } } indexed.into_boxed_slice() } } pub struct MessageCache { /// max 128 cached message signatures. Most recent FIRST. /// Server should (when possible) reference indexes in this (recipient's) cache instead of sending full signatures in last seen. /// Must be 1:1 with client's signature cache. full_cache: VecDeque>, /// max 20 last seen messages by the sender. Most Recent LAST pub last_seen: LastSeen, } impl Default for MessageCache { fn default() -> Self { Self { full_cache: VecDeque::with_capacity(MAX_CACHED_SIGNATURES as usize), last_seen: LastSeen::default(), } } } impl MessageCache { /// Not used for caching seen messages. Only for non-indexed signatures from senders. pub fn cache_signatures(&mut self, signatures: &[Box<[u8]>]) { for sig in signatures.iter().rev() { if self.full_cache.contains(sig) { continue; } // If the cache is maxed, and someone sends a signature older than the oldest in cache, ignore it if self.full_cache.len() < MAX_CACHED_SIGNATURES as usize { self.full_cache.push_back(sig.clone()); // Recipient never saw this message so it must be older than the oldest in cache } } } /// Adds a seen signature to `last_seen` and `full_cache`. pub fn add_seen_signature(&mut self, signature: &[u8]) { if self.last_seen.0.len() >= MAX_PREVIOUS_MESSAGES as usize { self.last_seen.0.remove(0); } self.last_seen.0.push(signature.into()); // This probably doesn't need to be a loop, but better safe than sorry while self.full_cache.len() >= MAX_CACHED_SIGNATURES as usize { self.full_cache.pop_back(); } self.full_cache.push_front(signature.into()); // Since recipient saw this message it will be most recent in cache } } impl InventoryPlayer for Player { fn drop_item(&self, item: ItemStack, _retain_ownership: bool) -> PlayerFuture<'_, ()> { Box::pin(async move { self.drop_item(item).await; }) } // Synchronous methods remain unchanged fn has_infinite_materials(&self) -> bool { self.gamemode.load() == GameMode::Creative } fn get_inventory(&self) -> Arc { self.inventory.clone() } fn enqueue_inventory_packet<'a>( &'a self, packet: &'a CSetContainerContent, ) -> PlayerFuture<'a, ()> { Box::pin(async move { self.client.enqueue_packet(packet).await; }) } fn enqueue_slot_packet<'a>(&'a self, packet: &'a CSetContainerSlot) -> PlayerFuture<'a, ()> { Box::pin(async move { self.client.enqueue_packet(packet).await; }) } fn enqueue_cursor_packet<'a>(&'a self, packet: &'a CSetCursorItem) -> PlayerFuture<'a, ()> { Box::pin(async move { self.client.enqueue_packet(packet).await; }) } fn enqueue_property_packet<'a>( &'a self, packet: &'a CSetContainerProperty, ) -> PlayerFuture<'a, ()> { Box::pin(async move { self.client.enqueue_packet(packet).await; }) } fn enqueue_slot_set_packet<'a>( &'a self, packet: &'a CSetPlayerInventory, ) -> PlayerFuture<'a, ()> { Box::pin(async move { self.client.enqueue_packet(packet).await; }) } fn enqueue_set_held_item_packet<'a>( &'a self, packet: &'a CSetSelectedSlot, ) -> PlayerFuture<'a, ()> { Box::pin(async move { self.client.enqueue_packet(packet).await; }) } fn enqueue_equipment_change<'a>( &'a self, slot: &'a EquipmentSlot, stack: &'a ItemStack, ) -> PlayerFuture<'a, ()> { Box::pin(async move { let chunk_pos = self.living_entity.entity.chunk_pos.load(); self.world() .broadcast_to_chunk_except( chunk_pos, &[self.get_entity().entity_uuid], &CSetEquipment::new( self.entity_id().into(), vec![( slot.discriminant(), ItemStackSerializer::from(stack.clone()), )], ), ) .await; if let Some(equippable) = stack.get_data_component::() { self.world() .play_sound_event( equippable.equip_sound.clone(), SoundCategory::Players, &self.position(), ) .await; } }) } fn award_experience(&self, amount: i32) -> PlayerFuture<'_, ()> { Box::pin(async move { debug!("Player::award_experience called with amount={amount}"); if amount > 0 { debug!("Player: adding {amount} experience points"); if let Some(player) = self.world().get_player_by_uuid(self.gameprofile.id) { player.add_experience_points(amount).await; } } }) } }