Pumpkin/pumpkin/src/entity/player.rs
Alexander Medvedev 6f18d16581 fix: the chunk unloading issue
at least hopefully, i think it was an Ticket Overlap, By adding the new ticket before removing the old one, the ChunkLoading system always sees at least one valid path for the chunks that overlap between the two view distances.
The fix seems to made the system slower but atleast it seems to not bug anymore
2026-01-16 17:48:30 +01:00

2856 lines
102 KiB
Rust

use core::f32;
use std::collections::{BinaryHeap, HashSet, VecDeque};
use std::f64::consts::TAU;
use std::mem;
use std::num::NonZeroU8;
use std::ops::AddAssign;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicI32, AtomicI64, AtomicU8, AtomicU32, Ordering};
use std::time::{Duration, Instant};
use crossbeam::atomic::AtomicCell;
use crossbeam::channel::Receiver;
use log::warn;
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, RwLock};
use tokio::task::JoinHandle;
use uuid::Uuid;
use pumpkin_data::damage::DamageType;
use pumpkin_data::data_component_impl::{AttributeModifiersImpl, Operation};
use pumpkin_data::data_component_impl::{EquipmentSlot, EquippableImpl};
use pumpkin_data::effect::StatusEffect;
use pumpkin_data::entity::{EntityPose, EntityStatus, EntityType};
use pumpkin_data::particle::Particle;
use pumpkin_data::sound::{Sound, SoundCategory};
use pumpkin_data::tag::Taggable;
use pumpkin_data::{Block, BlockState, tag};
use pumpkin_inventory::player::{
player_inventory::PlayerInventory, player_screen_handler::PlayerScreenHandler,
};
use pumpkin_inventory::screen_handler::{
InventoryPlayer, PlayerFuture, ScreenHandler, ScreenHandlerFactory, ScreenHandlerListener,
};
use pumpkin_inventory::sync_handler::SyncHandler;
use pumpkin_macros::send_cancellable;
use pumpkin_nbt::compound::NbtCompound;
use pumpkin_nbt::tag::NbtTag;
use pumpkin_protocol::IdOr;
use pumpkin_protocol::codec::var_int::VarInt;
use pumpkin_protocol::java::client::play::{
Animation, CAcknowledgeBlockChange, CActionBar, CChangeDifficulty, CChunkBatchEnd,
CChunkBatchStart, CChunkData, CCloseContainer, CCombatDeath, 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, CTitleAnimation, CTitleText, CUnloadChunk, CUpdateMobEffect,
CUpdateTime, GameEvent, Metadata, PlayerAction, PlayerInfoFlags, PreviousMessage,
};
use pumpkin_protocol::java::server::play::SClickSlot;
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::item::ItemStack;
use pumpkin_world::level::{Level, SyncChunk, SyncEntityChunk};
use crate::block::blocks::bed::BedBlock;
use crate::command::client_suggestions;
use crate::command::dispatcher::CommandDispatcher;
use crate::data::op_data::OPERATOR_CONFIG;
use crate::entity::{EntityBaseFuture, NbtFuture, TeleportFuture};
use crate::net::{ClientPlatform, GameProfile};
use crate::net::{DisconnectReason, PlayerConfig};
use crate::plugin::player::player_change_world::PlayerChangeWorldEvent;
use crate::plugin::player::player_gamemode_change::PlayerGamemodeChangeEvent;
use crate::plugin::player::player_teleport::PlayerTeleportEvent;
use crate::server::Server;
use crate::world::World;
use crate::{PERMISSION_MANAGER, block};
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 = 4671; // 1.21.11
enum BatchState {
Initial,
Waiting,
Count(u8),
}
struct HeapNode(i32, Vector2<i32>, SyncChunk);
impl Eq for HeapNode {}
impl PartialEq<Self> for HeapNode {
fn eq(&self, other: &Self) -> bool {
self.0 == other.0
}
}
impl PartialOrd<Self> for HeapNode {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
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<i32>,
view_distance: u8,
chunk_listener: Receiver<(Vector2<i32>, SyncChunk)>,
chunk_sent: HashSet<Vector2<i32>>,
chunk_queue: BinaryHeap<HeapNode>,
entity_chunk_queue: VecDeque<(Vector2<i32>, SyncEntityChunk)>,
batches_sent_since_ack: BatchState,
}
impl ChunkManager {
pub const NOTCHIAN_BATCHES_WITHOUT_ACK_UNTIL_PAUSE: u8 = 10;
#[must_use]
pub fn new(
chunks_per_tick: usize,
chunk_listener: Receiver<(Vector2<i32>, SyncChunk)>,
) -> Self {
Self {
chunks_per_tick,
center: Vector2::<i32>::new(0, 0),
view_distance: 0,
chunk_listener,
chunk_sent: HashSet::new(),
chunk_queue: BinaryHeap::new(),
entity_chunk_queue: VecDeque::new(),
batches_sent_since_ack: BatchState::Initial,
}
}
pub fn pull_new_chunks(&mut self) {
// log::debug!("pull new chunks");
while let Ok((pos, chunk)) = self.chunk_listener.try_recv() {
let dst = (pos.x - self.center.x)
.abs()
.max((pos.y - self.center.y).abs());
if dst > i32::from(self.view_distance) {
continue;
}
if self.chunk_sent.insert(pos) {
// log::debug!("receive new chunk {pos:?}");
self.chunk_queue.push(HeapNode(dst, pos, chunk));
}
}
// log::debug!("chunk_queue size {}", self.chunk_queue.len());
// log::debug!("chunk_sent size {}", self.chunk_sent.len());
}
pub fn update_center_and_view_distance(
&mut self,
center: Vector2<i32>,
mut view_distance: u8,
level: &Arc<Level>,
) {
view_distance += 1;
let old_center = self.center;
let old_view_distance = self.view_distance;
{
let mut lock = level.chunk_loading.lock().unwrap();
lock.add_ticket(
center,
ChunkLoading::get_level_from_view_distance(view_distance),
);
if old_center != center || old_view_distance != view_distance {
lock.remove_ticket(
old_center,
ChunkLoading::get_level_from_view_distance(old_view_distance),
);
}
lock.send_change();
};
let view_distance_i32 = i32::from(view_distance);
let mut chunks_to_watch = Vec::new();
let mut chunks_to_unwatch = Vec::new();
for pos in &self.chunk_sent {
if (pos.x - center.x).abs().max((pos.y - center.y).abs()) > view_distance_i32 {
chunks_to_unwatch.push(*pos);
}
}
let level_clone = level.clone();
let chunks_to_unwatch_clone = chunks_to_unwatch.clone();
tokio::spawn(async move {
level_clone
.mark_chunks_as_not_watched(&chunks_to_unwatch_clone)
.await;
});
self.chunk_sent.retain(|pos| {
(pos.x - center.x).abs().max((pos.y - center.y).abs()) <= view_distance_i32
});
let mut new_queue = BinaryHeap::with_capacity(self.chunk_queue.len());
for node in &self.chunk_queue {
let dst = (node.1.x - center.x).abs().max((node.1.y - center.y).abs());
if dst <= view_distance_i32 {
new_queue.push(HeapNode(dst, node.1, node.2.clone()));
}
}
self.chunk_queue = new_queue;
self.center = center;
self.view_distance = view_distance;
for dx in (-view_distance_i32)..=view_distance_i32 {
for dy in (-view_distance_i32)..=view_distance_i32 {
let new_pos = center.add_raw(dx, dy);
if !self.chunk_sent.contains(&new_pos) {
chunks_to_watch.push(new_pos);
if let Some(chunk) = level.loaded_chunks.get(&new_pos) {
self.push_chunk(new_pos, chunk.value().clone());
}
}
}
}
let level_clone = level.clone();
tokio::spawn(async move {
level_clone
.mark_chunks_as_newly_watched(&chunks_to_watch)
.await;
});
}
pub fn clean_up(&mut self, level: &Arc<Level>) {
let mut lock = level.chunk_loading.lock().unwrap();
lock.remove_ticket(
self.center,
ChunkLoading::get_level_from_view_distance(self.view_distance),
);
let (_rx, tx) = crossbeam::channel::unbounded();
// drop old channel
self.chunk_listener = tx;
}
pub fn change_world(&mut self, old_level: &Arc<Level>, new_level: &Arc<Level>) {
let mut lock = old_level.chunk_loading.lock().unwrap();
lock.remove_ticket(
self.center,
ChunkLoading::get_level_from_view_distance(self.view_distance),
);
drop(lock);
self.chunk_listener = new_level.chunk_listener.add_global_chunk_listener();
self.chunk_sent.clear();
self.chunk_queue.clear();
}
pub fn handle_acknowledge(&mut self, chunks_per_tick: f32) {
self.batches_sent_since_ack = BatchState::Count(0);
self.chunks_per_tick = chunks_per_tick.ceil() as usize;
}
pub fn push_chunk(&mut self, position: Vector2<i32>, chunk: SyncChunk) {
self.chunk_sent.insert(position);
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<i32>, chunk: SyncEntityChunk) {
self.entity_chunk_queue.push_back((position, chunk));
}
#[must_use]
pub fn can_send_chunk(&self) -> bool {
let state_available = match self.batches_sent_since_ack {
BatchState::Count(count) => count < Self::NOTCHIAN_BATCHES_WITHOUT_ACK_UNTIL_PAUSE,
BatchState::Initial => true,
BatchState::Waiting => false,
};
state_available && !self.chunk_queue.is_empty()
}
pub fn next_chunk(&mut self) -> Box<[SyncChunk]> {
let mut chunk_size = self.chunk_queue.len().min(self.chunks_per_tick);
let mut chunks = Vec::<Arc<RwLock<ChunkData>>>::with_capacity(chunk_size);
while chunk_size > 0 {
chunks.push(self.chunk_queue.pop().unwrap().2);
chunk_size -= 1;
}
match &mut self.batches_sent_since_ack {
BatchState::Count(count) => {
count.add_assign(1);
}
state @ BatchState::Initial => *state = BatchState::Waiting,
BatchState::Waiting => (),
}
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);
let chunks: Vec<Arc<RwLock<ChunkEntityData>>> = self
.entity_chunk_queue
.drain(0..chunk_size)
.map(|(_, chunk)| chunk)
.collect();
match &mut self.batches_sent_since_ack {
BatchState::Count(count) => {
count.add_assign(1);
}
state @ BatchState::Initial => *state = BatchState::Waiting,
BatchState::Waiting => unreachable!(),
}
chunks.into_boxed_slice()
}
}
/// 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<PlayerInventory>,
/// The player's `EnderChest` inventory.
pub ender_chest_inventory: Arc<EnderChestInventory>,
/// The player's configuration settings. Changes when the player changes their settings.
pub config: RwLock<PlayerConfig>,
/// The player's current gamemode (e.g., Survival, Creative, Adventure).
pub gamemode: AtomicCell<GameMode>,
/// The player's previous gamemode
pub previous_gamemode: AtomicCell<Option<GameMode>>,
/// The player's spawnpoint
pub respawn_point: AtomicCell<Option<RespawnPoint>>,
/// The player's sleep status
pub sleeping_since: AtomicCell<Option<u8>>,
/// Manages the player's hunger level.
pub hunger_manager: HungerManager,
/// The ID of the currently open container (if any).
pub open_container: AtomicCell<Option<u64>>,
/// The item currently being held by the player.
pub carried_item: Mutex<Option<ItemStack>>,
/// 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<Abilities>,
/// 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<BlockPos>,
/// 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<Option<(VarInt, Vector3<f64>)>>,
/// The coordinates of the chunk section the player is currently watching.
pub watched_section: AtomicCell<Cylindrical>,
/// 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<Instant>,
/// 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<PermissionLvl>,
/// 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,
/// The player's experience level.
pub experience_level: AtomicI32,
/// The player's experience progress (`0.0` to `1.0`)
pub experience_progress: AtomicCell<f32>,
/// The player's total experience points.
pub experience_points: AtomicI32,
pub experience_pick_up_delay: Mutex<u32>,
pub chunk_manager: Mutex<ChunkManager>,
pub has_played_before: AtomicBool,
pub chat_session: Arc<Mutex<ChatSession>>,
pub signature_cache: Mutex<MessageCache>,
pub player_screen_handler: Arc<Mutex<PlayerScreenHandler>>,
pub current_screen_handler: Mutex<Arc<Mutex<dyn ScreenHandler>>>,
pub screen_handler_sync_id: AtomicU8,
pub screen_handler_listener: Arc<dyn ScreenHandlerListener>,
pub screen_handler_sync_handler: Arc<SyncHandler>,
}
impl Player {
pub async fn new(
client: ClientPlatform,
gameprofile: GameProfile,
config: PlayerConfig,
world: Arc<World>,
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::new(
player_uuid,
world.clone(),
Vector3::new(0.0, 100.0, 0.0),
&EntityType::PLAYER,
matches!(gamemode, GameMode::Creative | GameMode::Spectator),
));
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,
));
Self {
living_entity,
config: RwLock::new(config),
gameprofile,
client,
awaiting_teleport: Mutex::new(None),
// TODO: Load this from previous instance
hunger_manager: HungerManager::default(),
current_block_destroy_stage: AtomicI32::new(-1),
open_container: 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::default()),
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_attacked_ticks: AtomicU32::new(0),
client_loaded: AtomicBool::new(false),
client_loaded_timeout: AtomicU32::new(60),
// Minecraft has no way to change the default permission level of new players.
// Minecraft's default permission level is 0.
permission_lvl: 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(),
)),
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()),
}
}
/// 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<JoinHandle<F::Output>>`. If the client is closed, this returns `None`.
pub fn spawn_task<F>(&self, task: F) -> Option<JoinHandle<F::Output>>
where
F: Future + Send + 'static,
F::Output: Send + 'static,
{
self.client.spawn_task(task)
}
pub fn inventory(&self) -> &Arc<PlayerInventory> {
&self.inventory
}
pub fn ender_chest_inventory(&self) -> &Arc<EnderChestInventory> {
&self.ender_chest_inventory
}
/// Removes the [`Player`] out of the current [`World`].
pub async fn remove(self: &Arc<Self>) {
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();
log::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).await;
// Remove left over entries from all possiblily loaded chunks
level.clean_memory();
log::debug!(
"Removed player id {} from world {} ({} chunks remain cached)",
self.gameprofile.name,
"world", // TODO: Add world names
level.loaded_chunk_count(),
);
level.clean_up_log().await;
//self.world().level.list_cached();
}
pub async fn attack(&self, victim: Arc<dyn EntityBase>) {
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 = 1.0;
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
// 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::<AttributeModifiersImpl>()
{
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 = 0.2 + attack_cooldown_progress.powi(2) * 0.8;
}
// 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.living_entity.entity),
Some(&self.living_entity.entity),
)
.await
{
world
.play_sound(
Sound::EntityPlayerAttackNodamage,
SoundCategory::Players,
&self.living_entity.entity.pos.load(),
)
.await;
return;
}
if victim.get_living_entity().is_some() {
let mut knockback_strength = 1.0;
player_attack_sound(&pos, world, attack_type).await;
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, world, victim_entity, knockback_strength)
.await;
}
}
if config.swing {}
}
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
}
pub async fn get_respawn_point(&self) -> Option<(Vector3<f64>, f32, f32)> {
let respawn_point = self.respawn_point.load()?;
let block = self.world().get_block(&respawn_point.position).await;
if respawn_point.dimension == Dimension::OVERWORLD
&& block.has_tag(&tag::Block::MINECRAFT_BEDS)
{
// TODO: calculate respawn position
Some((respawn_point.position.to_f64(), respawn_point.yaw, 0.0))
} else if respawn_point.dimension == Dimension::THE_NETHER
&& block == &Block::RESPAWN_ANCHOR
{
// TODO: calculate respawn position
// TODO: check if there is fuel for respawn
Some((respawn_point.position.to_f64(), respawn_point.yaw, 0.0))
} else {
self.client
.send_packet_now(&CGameEvent::new(GameEvent::NoRespawnBlockAvailable, 0.0))
.await;
None
}
}
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::DATA_SLEEPING_POSITION,
MetaDataType::OptionalBlockPos,
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
}
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::DATA_SLEEPING_POSITION,
MetaDataType::OptionalBlockPos,
None::<BlockPos>,
)])
.await;
world
.broadcast_packet_all(&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<f64>,
offset: Vector3<f32>,
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<f64>,
volume: f32,
pitch: f32,
seed: f64,
) {
self.client
.enqueue_packet(&CSoundEffect::new(
IdOr::Id(sound_id),
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<ResourceLocation>,
category: Option<SoundCategory>,
) {
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<Self>, 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 {
let chunk = chunk.read().await;
// 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 {
let chunk = chunk.read().await;
bedrock_client
.send_game_packet(&CLevelChunk {
dimension: 0,
cache_enabled: false,
chunk: &chunk,
})
.await;
}
}
}
}
self.tick_counter.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);
self.living_entity.tick(self.clone(), server).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();
// TODO This should only be handled by the ClientPlatform
let now = Instant::now();
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("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<f64>) {
// 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<World> {
&self.living_entity.entity.world
}
pub fn position(&self) -> Vector3<f64> {
self.living_entity.entity.pos.load()
}
pub fn eye_position(&self) -> Vector3<f64> {
let eye_height = f64::from(self.living_entity.entity.entity_dimension.load().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 mut ability_value = 0;
let abilities = &self.abilities.lock().await;
if abilities.invulnerable {
ability_value |= 1 << Ability::Invulnerable as u32;
}
if abilities.flying {
ability_value |= 1 << Ability::Flying as u32;
}
if abilities.allow_flying {
ability_value |= 1 << Ability::MayFly as u32;
}
if abilities.creative {
ability_value |= 1 << Ability::OperatorCommands as u32;
ability_value |= 1 << Ability::Teleport as u32;
ability_value |= 1 << Ability::Invulnerable as u32;
}
// Todo: Integrate this into the system
ability_value |= 1 << Ability::AttackMobs as u32;
ability_value |= 1 << Ability::AttackPlayers as u32;
ability_value |= 1 << Ability::Build as u32;
ability_value |= 1 << Ability::DoorsAndSwitches as u32;
ability_value |= 1 << Ability::Instabuild as u32;
ability_value |= 1 << Ability::Mine as u32;
let packet = CUpdateAbilities {
target_player_raw_id: self.entity_id().into(),
player_permission: 2,
command_permission: 4,
layers: vec![AbilityLayer {
serialized_layer: 1,
abilities_set: (1 << Ability::AbilityCount as u32) - 1,
ability_value,
fly_speed: 0.05,
vertical_fly_speed: 1.0,
walk_speed: 0.1,
}],
};
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::SetOpLevel0,
PermissionLvl::One => EntityStatus::SetOpLevel1,
PermissionLvl::Two => EntityStatus::SetOpLevel2,
PermissionLvl::Three => EntityStatus::SetOpLevel3,
PermissionLvl::Four => EntityStatus::SetOpLevel4,
};
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.read().await;
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<Self>,
lvl: PermissionLvl,
command_dispatcher: &CommandDispatcher,
) {
self.permission_lvl.store(lvl);
self.send_permission_lvl_update().await;
client_suggestions::send_c_commands_packet(self, 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;
}
}
}
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<Self>,
new_world: Arc<World>,
position: Vector3<f64>,
yaw: Option<f32>,
pitch: Option<f32>,
) {
let current_world = self.living_entity.entity.world.clone();
let yaw = yaw.unwrap_or(new_world.level_info.read().await.spawn_yaw);
let pitch = pitch.unwrap_or(new_world.level_info.read().await.spawn_pitch);
send_cancellable! {{
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 uuid = self.gameprofile.id;
current_world.remove_player(self, false).await;
new_world.players.write().await.insert(uuid, Arc::new(Self::new(
self.client.clone(),
self.gameprofile.clone(),
self.config.read().await.clone(),
new_world.clone(),
self.gamemode.load(),
)
.await));
self.unload_watched_chunks(&current_world).await;
self.chunk_manager.lock().await.change_world(&current_world.level, &new_world.level);
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(),
ResourceLocation::from(new_world.dimension.minecraft_name),
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;
self.clone().request_teleport(position, yaw, pitch).await;
self.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(self, 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<Self>, position: Vector3<f64>, 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.
send_cancellable! {{
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 standing_eye_height = self.living_entity.entity.entity_dimension.load().eye_height;
box_pos.squared_magnitude(Vector3 {
x: entity_pos.x,
y: entity_pos.y + f64::from(standing_eye_height),
z: entity_pos.z,
}) < d * d
}
pub async fn kick(&self, reason: DisconnectReason, message: TextComponent) {
self.client.kick(reason, message).await;
}
pub fn can_food_heal(&self) -> bool {
let health = self.living_entity.health.load();
let max_health = 20.0; // TODO
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) {
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) {
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 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
.read()
.await
.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;
}
}
self.client
.send_packet_now(&CCombatDeath::new(self.entity_id().into(), &death_msg))
.await;
}
pub async fn set_gamemode(self: &Arc<Self>, gamemode: GameMode) {
// 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"
);
send_cancellable! {{
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;
self.living_entity.entity.invulnerable.store(
matches!(gamemode, GameMode::Creative | GameMode::Spectator),
Ordering::Relaxed,
);
self.living_entity
.entity
.world
.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;
}
}}
}
/// Send the player's skin layers and used hand to all players.
pub async fn send_client_information(&self) {
let config = self.config.read().await;
self.living_entity
.entity
.send_meta_data(&[
Metadata::new(
TrackedData::DATA_PLAYER_MODE_CUSTOMIZATION_ID,
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 *= 1.0 + (self.get_haste_amplifier().await + 1) as f32 * 0.2;
}
// 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;
}
pub async fn drop_item(&self, item_stack: ItemStack) {
let item_pos = self.living_entity.entity.pos.load()
+ Vector3::new(0.0, f64::from(EntityType::PLAYER.eye_height) - 0.3, 0.0);
let entity = Entity::new(
Uuid::new_v4(),
self.world().clone(),
item_pos,
&EntityType::ITEM,
false,
);
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::<f64>() * TAU;
let l = 0.02 * rand::random::<f64>();
let velocity = Vector3::new(
-yaw_sin * pitch_cos * 0.3 + horizontal_offset.cos() * l,
-pitch_sin * 0.3 + 0.1 + (rand::random::<f64>() - rand::random::<f64>()) * 0.1,
yaw_cos * pitch_cos * 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) {
// should be locked first otherwise cause deadlock in tick() (this thread lock stack, that thread lock screen_handler)
let binding = self.inventory.held_item();
let mut item_stack = binding.lock().await;
if !item_stack.is_empty() {
let drop_amount = if drop_stack { item_stack.item_count } else { 1 };
self.drop_item(item_stack.copy_with_count(drop_amount))
.await;
item_stack.decrement(drop_amount);
let selected_slot = self.inventory.get_selected_slot();
let inv: Arc<dyn Inventory> = self.inventory.clone();
let screen_binding = self.current_screen_handler.lock().await;
let mut screen_handler = screen_binding.lock().await;
let slot_index = screen_handler
.get_slot_index(&inv, selected_slot as usize)
.await;
if let Some(slot_index) = slot_index {
screen_handler.set_received_stack(slot_index, item_stack.clone());
}
}
}
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) {
match &self.client {
ClientPlatform::Java(client) => {
client
.enqueue_packet(&CSystemChatMessage::new(text, false))
.await;
}
ClientPlatform::Bedrock(client) => {
client
.send_game_packet(&SText::system_message(text.clone().get_text()))
.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) {
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),
points.into(),
level.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;
self.client
.enqueue_packet(&CSetExperience::new(
progress.clamp(0.0, 1.0),
points.into(),
level.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.send_effect(effect.clone()).await;
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;
}
}
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) {
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_effect(&self) -> u8 {
let mut count = 0;
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;
count += 1;
}
//Need to remove effect after because the player effect are lock in the for before
for effect in effect_list {
self.living_entity.remove_effect(effect).await;
}
count
}
/// 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, added_points: i32) {
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 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) {
self.current_screen_handler
.lock()
.await
.lock()
.await
.on_closed(self)
.await;
let player_screen_handler: Arc<Mutex<dyn ScreenHandler>> =
self.player_screen_handler.clone();
let current_screen_handler: Arc<Mutex<dyn ScreenHandler>> =
self.current_screen_handler.lock().await.clone();
if !Arc::ptr_eq(&player_screen_handler, &current_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();
}
pub async fn on_screen_handler_opened(&self, screen_handler: Arc<Mutex<dyn ScreenHandler>>) {
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,
) -> Option<u8> {
if !self
.current_screen_handler
.lock()
.await
.lock()
.await
.as_any()
.is::<PlayerScreenHandler>()
{
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;
Some(self.screen_handler_sync_id.load(Ordering::Relaxed))
} else {
//TODO: Send message if spectator
None
}
}
pub async fn on_slot_click(&self, packet: SClickSlot) {
let screen_handler = self.current_screen_handler.lock().await;
let mut screen_handler = screen_handler.lock().await;
let behaviour = screen_handler.get_behaviour();
// behaviour is dropped here
if i32::from(behaviour.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) {
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;
}
let not_in_sync = packet.revision.0 != (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,
)
.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;
drop(screen_handler);
}
}
/// Check if the player has a specific permission
pub async fn has_permission(&self, node: &str) -> bool {
let perm_manager = PERMISSION_MANAGER.read().await;
perm_manager
.has_permission(&self.gameprofile.id, node, self.permission_lvl.load())
.await
}
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 animation = match hand {
Hand::Left => Animation::SwingMainArm,
Hand::Right => Animation::SwingOffhand,
};
let packet = CEntityAnimation::new(entity_id, animation);
if all {
world.broadcast_packet_all(&packet).await;
} else {
world
.broadcast_packet_except(&[self.gameprofile.id], &packet)
.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 NbtCompound) -> NbtFuture<'a, ()> {
Box::pin(async move {
nbt.put_int("DataVersion", DATA_VERSION);
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;
// Store total XP instead of individual components
let total_exp =
experience::points_to_level(self.experience_level.load(Ordering::Relaxed))
+ self.experience_points.load(Ordering::Relaxed);
nbt.put_int("XpTotal", total_exp);
nbt.put_byte("playerGameType", self.gamemode.load() as i8);
if let Some(previous_gamemode) = self.previous_gamemode.load() {
nbt.put_byte("previousPlayerGameType", previous_gamemode as i8);
}
nbt.put_bool(
"HasPlayedBefore",
self.has_played_before.load(Ordering::Relaxed),
);
// Store food level, saturation, exhaustion, and tick timer
self.hunger_manager.write_nbt(nbt).await;
nbt.put_string(
"Dimension",
ResourceLocation::from(self.world().dimension.minecraft_name).to_string(),
);
})
}
fn read_nbt<'a>(&'a mut self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> {
Box::pin(async move {
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;
self.gamemode.store(
GameMode::try_from(nbt.get_byte("playerGameType").unwrap_or(0))
.unwrap_or(GameMode::Survival),
);
self.previous_gamemode.store(
nbt.get_byte("previousPlayerGameType")
.and_then(|byte| GameMode::try_from(byte).ok()),
);
self.has_played_before.store(
nbt.get_bool("HasPlayedBefore").unwrap_or(false),
Ordering::Relaxed,
);
// Load food level, saturation, exhaustion, and tick timer
self.hunger_manager.read_nbt(nbt).await;
// Load from total XP
let total_exp = nbt.get_int("XpTotal").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);
})
}
}
impl NBTStorageInit for Player {}
impl NBTStorage for PlayerInventory {
fn write_nbt<'a>(&'a self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> {
Box::pin(async move {
// Save the selected slot (hotbar)
nbt.put_int("SelectedItemSlot", i32::from(self.get_selected_slot()));
// Create inventory list with the correct capacity (inventory size)
let mut items: Vec<NbtTag> = Vec::with_capacity(41);
for (i, item) in self.main_inventory.iter().enumerate() {
let stack = item.lock().await;
if !stack.is_empty() {
let mut item_compound = NbtCompound::new();
item_compound.put_byte("Slot", i as i8);
stack.write_item_stack(&mut item_compound);
drop(stack);
items.push(NbtTag::Compound(item_compound));
}
}
let mut equipment_compound = NbtCompound::new();
for slot in self.equipment_slots.values() {
let stack_binding = self.entity_equipment.lock().await.get(slot);
let stack = stack_binding.lock().await;
if !stack.is_empty() {
let mut item_compound = NbtCompound::new();
stack.write_item_stack(&mut item_compound);
drop(stack);
match slot {
EquipmentSlot::OffHand(_) => {
equipment_compound.put_component("offhand", item_compound);
}
EquipmentSlot::Feet(_) => {
equipment_compound.put_component("feet", item_compound);
}
EquipmentSlot::Legs(_) => {
equipment_compound.put_component("legs", item_compound);
}
EquipmentSlot::Chest(_) => {
equipment_compound.put_component("chest", item_compound);
}
EquipmentSlot::Head(_) => {
equipment_compound.put_component("head", item_compound);
}
_ => {
warn!("Invalid equipment slot for a player");
}
}
}
}
nbt.put_component("equipment", equipment_compound);
nbt.put("Inventory", NbtTag::List(items));
})
}
fn read_nbt_non_mut<'a>(&'a self, nbt: &'a NbtCompound) -> NbtFuture<'a, ()> {
Box::pin(async {
// Read selected hotbar slot
self.set_selected_slot(nbt.get_int("SelectedItemSlot").unwrap_or(0) as u8);
// Process inventory list
if let Some(inventory_list) = nbt.get_list("Inventory") {
for tag in inventory_list {
if let Some(item_compound) = tag.extract_compound()
&& let Some(slot_byte) = item_compound.get_byte("Slot")
{
let slot = slot_byte as usize;
if let Some(item_stack) = ItemStack::read_item_stack(item_compound) {
self.set_stack(slot, item_stack).await;
}
}
}
}
if let Some(equipment) = nbt.get_compound("equipment") {
if let Some(offhand) = equipment.get_compound("offhand")
&& let Some(item_stack) = ItemStack::read_item_stack(offhand)
{
self.set_stack(40, item_stack).await;
}
if let Some(head) = equipment.get_compound("head")
&& let Some(item_stack) = ItemStack::read_item_stack(head)
{
self.set_stack(39, item_stack).await;
}
if let Some(chest) = equipment.get_compound("chest")
&& let Some(item_stack) = ItemStack::read_item_stack(chest)
{
self.set_stack(38, item_stack).await;
}
if let Some(legs) = equipment.get_compound("legs")
&& let Some(item_stack) = ItemStack::read_item_stack(legs)
{
self.set_stack(37, item_stack).await;
}
if let Some(feet) = equipment.get_compound("feet")
&& let Some(item_stack) = ItemStack::read_item_stack(feet)
{
self.set_stack(36, item_stack).await;
}
}
})
}
}
impl NBTStorageInit for PlayerInventory {}
impl NBTStorage for EnderChestInventory {
fn write_nbt<'a>(&'a self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> {
Box::pin(async {
// Create item list with the correct capacity (inventory size)
let mut items: Vec<NbtTag> = Vec::with_capacity(Self::INVENTORY_SIZE);
for (i, item) in self.items.iter().enumerate() {
let stack = item.lock().await;
if !stack.is_empty() {
let mut item_compound = NbtCompound::new();
item_compound.put_byte("Slot", i as i8);
stack.write_item_stack(&mut item_compound);
drop(stack);
items.push(NbtTag::Compound(item_compound));
}
}
nbt.put("EnderItems", NbtTag::List(items));
})
}
fn read_nbt_non_mut<'a>(&'a self, nbt: &'a NbtCompound) -> NbtFuture<'a, ()> {
Box::pin(async {
// Process item list
if let Some(item_list) = nbt.get_list("EnderItems") {
for tag in item_list {
if let Some(item_compound) = tag.extract_compound()
&& let Some(slot_byte) = item_compound.get_byte("Slot")
{
let slot = slot_byte as usize;
if let Some(item_stack) = ItemStack::read_item_stack(item_compound) {
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<Vector3<f64>>,
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 {
return false;
}
let dyn_self = self
.living_entity
.entity
.world
.get_entity_by_id(self.living_entity.entity.entity_id)
.await
.expect("Entity not found in world");
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(&*dyn_self, damage_type, source, cause)
.await;
self.handle_killed(death_message).await;
}
}
result
})
}
fn teleport(
self: Arc<Self>,
position: Vector3<f64>,
yaw: Option<f32>,
pitch: Option<f32>,
world: Arc<World>,
) -> 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());
send_cancellable! {{
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;
entity
.world
.broadcast_packet_except(&[self.gameprofile.id], &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> {
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),
));
Box::pin(async move { name.insertion(self.gameprofile.name.clone()) })
}
fn as_nbt_storage(&self) -> &dyn NBTStorage {
self
}
}
#[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 NbtCompound) -> NbtFuture<'a, ()> {
Box::pin(async {
let mut component = NbtCompound::new();
component.put_bool("invulnerable", self.invulnerable);
component.put_bool("flying", self.flying);
component.put_bool("mayfly", self.allow_flying);
component.put_bool("instabuild", self.creative);
component.put_bool("mayBuild", self.allow_modify_world);
component.put_float("flySpeed", self.fly_speed);
component.put_float("walkSpeed", self.walk_speed);
nbt.put_component("abilities", component);
})
}
fn read_nbt<'a>(&'a mut self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> {
Box::pin(async move {
if let Some(component) = nbt.get_compound("abilities") {
self.invulnerable = component.get_bool("invulnerable").unwrap_or(false);
self.flying = component.get_bool("flying").unwrap_or(false);
self.allow_flying = component.get_bool("mayfly").unwrap_or(false);
self.creative = component.get_bool("instabuild").unwrap_or(false);
self.allow_modify_world = component.get_bool("mayBuild").unwrap_or(false);
self.fly_speed = component.get_float("flySpeed").unwrap_or(0.05);
self.walk_speed = component.get_float("walkSpeed").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 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 respawn point.
#[derive(Copy, Debug, Clone, PartialEq)]
pub struct RespawnPoint {
pub dimension: Dimension,
pub position: BlockPos,
pub yaw: f32,
pub force: bool,
}
/// 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<i32> for ChatMode {
type Error = InvalidChatMode;
fn try_from(value: i32) -> Result<Self, Self::Error> {
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<Box<[u8]>>,
}
impl Default for ChatSession {
fn default() -> Self {
Self::new(Uuid::nil(), 0, Box::new([]), Box::new([]))
}
}
impl ChatSession {
#[must_use]
pub 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<Box<[u8]>>);
impl From<LastSeen> for Vec<Box<[u8]>> {
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<Player>) -> 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<Box<[u8]>>,
/// 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<PlayerInventory> {
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 {
self.world()
.broadcast_packet_except(
&[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::<EquippableImpl>()
&& let Some(sound) = Sound::from_name(
equippable
.equip_sound
.strip_prefix("minecraft:")
.unwrap_or(equippable.equip_sound),
)
{
self.world()
.play_sound(sound, SoundCategory::Players, &self.position())
.await;
}
})
}
}