Skip to main content

steel_core/player/
mod.rs

1//! This module contains all things player-related.
2mod abilities;
3pub mod chat;
4pub mod chunk_sender;
5/// This module contains the `PlayerConnection` trait that abstracts network connections.
6pub mod connection;
7mod container_counter;
8mod entity_state;
9/// Experience System
10pub mod experience;
11pub mod food_data;
12/// Game mode specific logic for player interactions.
13pub mod game_mode;
14mod health_sync;
15mod item_cooldowns;
16mod lifecycle;
17pub mod movement;
18mod permissions;
19pub mod player_data;
20pub mod player_data_storage;
21pub mod player_inventory;
22mod profile;
23mod session;
24mod sleep;
25mod sleep_state;
26mod spam_throttler;
27pub mod stats_counter;
28mod tick_state;
29mod title;
30
31pub use abilities::{Abilities, DEFAULT_FLYING_SPEED};
32use chat::ChatState;
33pub use chat::{LastSeen, LastSeenMessagesValidator, MessageCache};
34use connection::NetworkConnection as _;
35pub use connection::{ClientInformation, PlayerConnection};
36use container_counter::ContainerCounter;
37use food_data::{FoodData, food_constants};
38use game_mode::{BlockBreakingManager, PlayerGameModeState};
39use glam::DVec3;
40use health_sync::HealthSyncState;
41use item_cooldowns::ItemCooldowns;
42use lifecycle::PlayerLifecycleState;
43pub use lifecycle::PlayerRespawnConfig;
44pub(crate) use lifecycle::ResetReason;
45pub use movement::PlayerInput;
46use movement::{MovementState, TeleportState};
47use permissions::PlayerPermissionState;
48pub(crate) use profile::{GAME_PROFILE_CACHE_LIMIT, KnownPlayerNameLookup, lookup_online_profile};
49pub use profile::{
50    GameProfile, GameProfileAction, KnownPlayer, KnownPlayers, ProfileLookupError,
51    is_valid_player_name, offline_uuid,
52};
53pub use session::PlayerSession;
54pub(crate) use session::PlayerSessionId;
55use simdnbt::owned::{NbtCompound, NbtList, NbtTag};
56use sleep_state::PlayerSleepState;
57use std::ptr;
58use std::sync::atomic::Ordering;
59use std::sync::{Arc, Weak};
60use std::time::{Duration, Instant};
61use steel_protocol::packets::game::{
62    CEntityEvent, CPlayerCombatKill, CPlayerLookAt, CRespawn, CSetDefaultSpawnPosition, CSetHealth,
63    CSetHeldSlot, CSetPassengers, ClientCommandAction, LookAtAnchor, RelativeMovement, SoundSource,
64};
65use steel_protocol::packets::game::{CLevelEvent, CSetEntityData, CSetExperience};
66use steel_registry::blocks::block_state_ext::BlockStateExt as _;
67use steel_registry::entity_data::{EntityPose, HumanoidArm, ParticleList};
68use steel_registry::entity_type::{EntityDimensions, EntityTypeRef};
69use steel_registry::game_rules::GameRuleRef;
70use steel_registry::sound_event::SoundEventRef;
71use steel_registry::vanilla_block_tags::BlockTag;
72use steel_registry::vanilla_entity_data::PlayerEntityData;
73use steel_registry::vanilla_game_rules::{
74    DROWNING_DAMAGE, FALL_DAMAGE, FIRE_DAMAGE, FREEZE_DAMAGE, IMMEDIATE_RESPAWN, KEEP_INVENTORY,
75    SHOW_DEATH_MESSAGES,
76};
77use steel_registry::{
78    level_events, sound_events, vanilla_attributes, vanilla_custom_stats, vanilla_damage_type_tags,
79    vanilla_entities, vanilla_game_events,
80};
81use steel_utils::{entity_events::EntityStatus, locks::Shared, translations};
82use tick_state::PlayerTickState;
83use uuid::Uuid;
84
85use arc_swap::ArcSwap;
86use steel_utils::locks::SyncMutex;
87use steel_utils::types::{Difficulty, GameType, InteractionHand};
88use text_components::resolving::TextResolutor;
89use text_components::translation::TranslatedMessage;
90use text_components::{
91    Modifier as _, TextComponent,
92    interactivity::{ClickEvent, HoverEvent},
93};
94use text_components::{content::Resolvable, custom::CustomData};
95
96use crate::behavior::{
97    BlockStateBehaviorExt as _, ITEM_BEHAVIORS, InteractionResult, ItemBehavior,
98    apply_use_remainder,
99};
100use crate::chunk::chunk_request::{ChunkRequestHandle, ChunkRequestState};
101use crate::config::RuntimeConfig;
102use crate::enchantment_helper;
103use crate::entity::damage::DamageSource;
104use crate::entity::entities::ExperienceOrbEntity;
105use crate::entity::{
106    ActiveItemUseState, DEATH_DURATION, Entity, EntityAnchor, EntityBase, EntityEventSource,
107    EntityMoveError, EntityMovementEmission, EntitySyncedData, LivingEntity, LivingEntityBase,
108    LivingEntitySyncedData, MobEffectSyncChange, MobEffectSyncPacket, RemovalReason, SharedEntity,
109    apply_entity_look_at, get_kill_credit, start_riding_entities,
110};
111use crate::fluid::get_fluid_state;
112use crate::inventory::equipment::{EntityEquipment, EquipmentSlot};
113use crate::inventory::lock::{ContainerLockGuard, ContainerRef};
114use crate::inventory::menu::Menu;
115use crate::inventory::menu::kinds::inventory_menu;
116use crate::level_data::RespawnData;
117use crate::permission::{
118    PermissionContext, PermissionExpr, PermissionMetadataSet, PermissionMetadataValue,
119    PermissionSet, PermissionState,
120};
121use crate::physics::MoveResult;
122use crate::player::experience::Experience;
123use crate::player::player_data::{PersistentEnderPearl, PersistentRootVehicle};
124use crate::player::player_inventory::{
125    MenuItemDisposition, MenuRemovalStatus, PlayerInventory, PlayerInventorySyncState,
126};
127use crate::server::{
128    PlayerPacketTransition, Server,
129    jobs::{JobPoll, ServerJob, ServerJobContext},
130};
131use crate::world::player_spawn_finder::{PlayerSpawnSearch, PlayerSpawnSearchPoll};
132use steel_registry::vanilla_damage_types;
133
134use steel_protocol::packets::{
135    common::SCustomPayload,
136    game::{CContainerClose, CGameEvent, CSystemChat, GameEventType},
137};
138use steel_registry::RegistryEntry;
139use steel_registry::item_stack::ItemStack;
140use steel_registry::items::ItemRef;
141use steel_registry::stat::vanilla_stat_types;
142use steel_utils::{
143    BlockPos, BlockStateId, ChunkPos, DowncastType, DowncastTypeKey, Identifier, UuidExt as _,
144};
145
146use crate::inventory::container::Container;
147
148const RESPAWN_SEARCH_READY_CANDIDATE_BUDGET: usize = 8;
149const HAT_MODEL_PART_MASK: i8 = 0b0100_0000;
150
151const DROP_SPAM_THROTTLER_INCREMENT_STEP: i32 = 20;
152const DROP_SPAM_THROTTLER_THRESHOLD: i32 = 1480;
153
154use crate::chunk::player_chunk_view::PlayerChunkView;
155use crate::entity::entities::objects::projectiles::FishingHookEntity;
156use crate::inventory::ender_chest::{PlayerEnderChestContainer, SyncPlayerEnderChest};
157use crate::player::chunk_sender::ChunkSender;
158use crate::player::stats_counter::StatsCounter;
159use crate::portal::{
160    PortalTicketTarget, TeleportPostAction, TeleportPostTransition, TeleportTransition,
161};
162use crate::world::World;
163
164/// A struct representing a player.
165pub struct Player {
166    /// The player's game profile.
167    pub gameprofile: GameProfile,
168    /// The player's connection (abstracted for testing).
169    pub connection: Arc<PlayerConnection>,
170    /// Stable connection session shared by every incarnation of this player.
171    pub(crate) session: Arc<PlayerSession>,
172
173    /// The world the player is in.
174    pub world: ArcSwap<World>,
175
176    /// Reference to the server (for entity ID generation, etc.).
177    pub(crate) server: Weak<Server>,
178    /// Runtime configuration shared with the server.
179    pub(crate) config: Arc<RuntimeConfig>,
180
181    /// Common entity fields (id, uuid, position, rotation, removal, callback).
182    base: EntityBase,
183
184    /// Client lifecycle flags.
185    lifecycle: SyncMutex<PlayerLifecycleState>,
186
187    /// Movement tracking state
188    pub(crate) movement: SyncMutex<MovementState>,
189
190    /// Synchronized entity data (health, pose, flags, etc.) for network sync.
191    entity_data: SyncMutex<PlayerEntityData>,
192
193    /// The last chunk position of the player.
194    pub last_chunk_pos: SyncMutex<ChunkPos>,
195    /// The last chunk tracking view of the player.
196    pub last_tracking_view: SyncMutex<Option<PlayerChunkView>>,
197    /// The client's settings/information (language, view distance, chat visibility, etc.).
198    /// Updated when the client sends `SClientInformation` during config or play phase.
199    client_information: SyncMutex<ClientInformation>,
200
201    /// Current and previous game mode.
202    game_modes: SyncMutex<PlayerGameModeState>,
203
204    /// The player's inventory container (shared with `inventory_menu`).
205    pub inventory: Shared<PlayerInventory>,
206
207    /// Logical inventory slots that must be resent directly to this player's client.
208    inventory_sync: SyncMutex<PlayerInventorySyncState>,
209
210    /// The player's ender chest inventory.
211    pub ender_chest_inventory: SyncPlayerEnderChest,
212
213    /// Last main-hand stack used for vanilla attack-strength reset checks.
214    last_item_in_main_hand: SyncMutex<ItemStack>,
215
216    /// The player's inventory menu (always open, even when `container_id` is 0).
217    inventory_menu: SyncMutex<Menu>,
218
219    /// The currently open menu (None if player inventory is open).
220    /// This is separate from `inventory_menu` which is always present.
221    open_menu: SyncMutex<player_inventory::OpenMenuState>,
222
223    /// Counter for generating container IDs (1-100, wraps around).
224    container_counter: SyncMutex<ContainerCounter>,
225
226    /// Pending server-initiated teleport state (ID, position, timeout).
227    teleport_state: SyncMutex<TeleportState>,
228    /// Vanilla item use cooldown groups.
229    item_cooldowns: SyncMutex<ItemCooldowns>,
230
231    /// Local tick and once-per-tick packet state.
232    tick_state: SyncMutex<PlayerTickState>,
233    /// Vanilla sleep/wake animation counter.
234    sleep_state: SyncMutex<PlayerSleepState>,
235    /// Persisted personal bed or respawn-anchor target.
236    respawn_config: SyncMutex<Option<PlayerRespawnConfig>>,
237
238    /// Player abilities (flight, invulnerability, build permissions, speeds, etc.)
239    pub abilities: SyncMutex<Abilities>,
240
241    /// Block breaking state machine.
242    pub block_breaking: SyncMutex<BlockBreakingManager>,
243
244    /// Shared living-entity runtime fields (attributes, speed, damage/death state).
245    /// Vanilla: `LivingEntity` (L230-232) + `Entity.invulnerableTime` (L256).
246    living_base: LivingEntityBase,
247
248    /// Player food/hunger state (food level, saturation, exhaustion).
249    pub food_data: SyncMutex<FoodData>,
250
251    /// Delta-tracking state for `CSetHealth` deduplication.
252    health_sync: SyncMutex<HealthSyncState>,
253
254    /// The Player's Experience
255    pub experience: SyncMutex<Experience>,
256
257    /// Assigned groups, direct overrides, and the effective permission set.
258    permissions: SyncMutex<PlayerPermissionState>,
259
260    /// Whether the player has completed the vanilla End credits flow.
261    seen_credits: SyncMutex<bool>,
262
263    /// Vanilla `ServerPlayer.wonGame`; transient while the End credits screen is open.
264    won_game: SyncMutex<bool>,
265
266    /// Monotonic counter bumped on world teleport/reset. The chunk sending tick
267    /// snapshots this before encoding and compares after to detect stale batches.
268    pub chunk_send_epoch: SyncMutex<u32>,
269
270    /// Domain-residence identity and persisted entities awaiting restoration.
271    residence: SyncMutex<PlayerResidenceState>,
272    /// In-flight ender pearls thrown by this player, kept weakly so they persist
273    /// with the player and re-spawn on login (vanilla `ServerPlayer.enderPearls`).
274    ender_pearls: SyncMutex<Vec<Weak<dyn Entity>>>,
275
276    /// Active fishing hook, kept weakly because the world owns live entities.
277    pub(crate) fishing: SyncMutex<Option<Weak<FishingHookEntity>>>,
278
279    /// The counter keeping track of this player's statistics.
280    stats: SyncMutex<StatsCounter>,
281
282    /// The last action time of this player.
283    last_action_time: SyncMutex<Instant>,
284}
285
286// SAFETY: This key is owned by Steel and uniquely identifies `Player`.
287unsafe impl DowncastType for Player {
288    const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:entity/player");
289}
290
291#[derive(Clone)]
292struct PendingRootVehicleRestore {
293    world: Identifier,
294    root_vehicle: PersistentRootVehicle,
295}
296
297/// Runtime identity for one continuous stay in a Steel domain.
298#[derive(Clone, Copy, Debug, Eq, PartialEq)]
299pub(crate) struct DomainResidenceToken(u64);
300
301#[derive(Clone)]
302struct PlayerResidenceState {
303    token: DomainResidenceToken,
304    pending_root_vehicle: Option<PendingRootVehicleRestore>,
305    pending_ender_pearls: Vec<PersistentEnderPearl>,
306}
307
308impl PlayerResidenceState {
309    const fn new() -> Self {
310        Self {
311            token: DomainResidenceToken(1),
312            pending_root_vehicle: None,
313            pending_ender_pearls: Vec::new(),
314        }
315    }
316
317    fn advance(&mut self) -> DomainResidenceToken {
318        let Some(next_token) = self.token.0.checked_add(1) else {
319            panic!("domain residence token space exhausted");
320        };
321        self.token = DomainResidenceToken(next_token);
322        self.pending_root_vehicle = None;
323        self.pending_ender_pearls.clear();
324        self.token
325    }
326}
327
328impl Player {
329    const USING_ITEM_FLAG: i8 = 1;
330    const OFF_HAND_ACTIVE_ITEM_FLAG: i8 = 1 << 1;
331
332    /// Returns the chunk sender owned by this player's connection session.
333    pub(crate) fn chunk_sender(&self) -> &SyncMutex<ChunkSender> {
334        &self.session.chunk_sender
335    }
336
337    /// Returns chat protocol state owned by this connection session.
338    pub(crate) fn chat(&self) -> &SyncMutex<ChatState> {
339        &self.session.chat
340    }
341
342    /// Returns the hand currently driving active item use.
343    #[must_use]
344    pub fn active_item_use_hand(&self) -> Option<InteractionHand> {
345        self.living_base
346            .active_item_use()
347            .map(|active| active.hand())
348    }
349
350    /// Starts using the item currently held in `hand`.
351    pub fn start_using_item(&self, hand: InteractionHand) {
352        let item = {
353            let inventory = self.inventory.lock();
354            let item = inventory.get_item_in_hand(hand);
355            item.clone()
356        };
357        let duration = ITEM_BEHAVIORS
358            .get_behavior(item.item())
359            .get_use_duration(&item, self);
360        if self.living_base.start_using_item(hand, &item, duration) {
361            let mut entity_data = self.entity_data.lock();
362            let flags = entity_data.living_entity().living_entity_flags.get();
363            let mut flags = *flags | Self::USING_ITEM_FLAG;
364            if hand == InteractionHand::OffHand {
365                flags |= Self::OFF_HAND_ACTIVE_ITEM_FLAG;
366            } else {
367                flags &= !Self::OFF_HAND_ACTIVE_ITEM_FLAG;
368            }
369            entity_data
370                .living_entity_mut()
371                .living_entity_flags
372                .set(flags);
373        }
374    }
375
376    fn stop_using_item(&self) {
377        self.living_base.stop_using_item();
378        let mut entity_data = self.entity_data.lock();
379        let flags = *entity_data.living_entity().living_entity_flags.get();
380        entity_data
381            .living_entity_mut()
382            .living_entity_flags
383            .set(flags & !Self::USING_ITEM_FLAG);
384    }
385
386    /// Releases the currently used item and invokes its release hook.
387    pub fn release_using_item(&self) {
388        let Some(active) = self.living_base.active_item_use() else {
389            return;
390        };
391        let behavior = ITEM_BEHAVIORS.get_behavior(active.item());
392        self.release_using_item_with_behavior(behavior, active);
393    }
394
395    fn release_using_item_with_behavior(
396        &self,
397        behavior: &dyn ItemBehavior,
398        active: ActiveItemUseState,
399    ) {
400        let hand = active.hand();
401        let item_matches = {
402            let inventory = self.inventory.lock();
403            inventory.get_item_in_hand(hand).item() == active.item()
404        };
405        if !item_matches {
406            self.stop_using_item();
407            return;
408        }
409        // Read a copy rather than clearing the slot,
410        // so any inventory-touching side effect from
411        // `release_using` never sees the hand as vacant.
412        let mut item = {
413            let inventory = self.inventory.lock();
414            let current = inventory.get_item_in_hand(hand);
415            current.clone()
416        };
417        let stack_before_using = item.clone();
418        let world = self.get_world();
419        let apply_side_effects =
420            behavior.release_using(&mut item, &world, self, active.remaining_ticks());
421        let use_on_release = behavior.use_on_release(&item);
422        if apply_side_effects {
423            item = apply_use_remainder(&stack_before_using, item, self);
424            self.apply_item_use_cooldown(&stack_before_using);
425        }
426        self.inventory.lock().set_item_in_hand(hand, item);
427        // we re-read active here since behavior.release_using might have already ended the use
428        if use_on_release && let Some(active) = self.living_base.active_item_use() {
429            self.tick_active_item_use_with_behavior(behavior, active);
430        }
431        self.stop_using_item();
432    }
433
434    fn tick_active_item_use(&self) {
435        let Some(active) = self.living_base.active_item_use() else {
436            return;
437        };
438        let behavior = ITEM_BEHAVIORS.get_behavior(active.item());
439        self.tick_active_item_use_with_behavior(behavior, active);
440    }
441
442    fn tick_active_item_use_with_behavior(
443        &self,
444        behavior: &dyn ItemBehavior,
445        active: ActiveItemUseState,
446    ) {
447        let hand = active.hand();
448        let item_matches = {
449            let inventory = self.inventory.lock();
450            inventory.get_item_in_hand(hand).item() == active.item()
451        };
452        if !item_matches {
453            self.stop_using_item();
454            return;
455        }
456        let mut item = {
457            let inventory = self.inventory.lock();
458            let current = inventory.get_item_in_hand(hand);
459            current.clone()
460        };
461        let world = self.get_world();
462        behavior.on_use_tick(&world, self, &mut item, active.remaining_ticks());
463
464        if self.active_item_use_hand() != Some(hand) {
465            self.inventory.lock().set_item_in_hand(hand, item);
466            return;
467        }
468        let Some(active) = self.living_base.decrement_active_item_use() else {
469            self.inventory.lock().set_item_in_hand(hand, item);
470            return;
471        };
472        let use_on_release = behavior.use_on_release(&item);
473        if active.remaining_ticks() == 0 && !use_on_release && !item.is_empty() {
474            let stack_before_finish = item.clone();
475            item = behavior.finish_using(&mut item, &world, self);
476            self.apply_item_use_cooldown(&stack_before_finish);
477            self.stop_using_item();
478        }
479
480        self.inventory.lock().set_item_in_hand(hand, item);
481    }
482
483    /// Returns the player's configured main arm.
484    #[must_use]
485    pub fn main_arm(&self) -> HumanoidArm {
486        self.client_information.lock().main_hand
487    }
488
489    #[must_use]
490    pub(crate) fn shows_hat(&self) -> bool {
491        let model_customization = *self
492            .entity_data
493            .lock()
494            .avatar()
495            .player_mode_customization
496            .get();
497        model_customization & HAT_MODEL_PART_MASK != 0
498    }
499
500    fn apply_client_information_to_entity_data(
501        data: &mut PlayerEntityData,
502        client_information: &ClientInformation,
503    ) {
504        let avatar = data.avatar_mut();
505        avatar.player_main_hand.set(client_information.main_hand);
506        avatar
507            .player_mode_customization
508            .set(client_information.model_customization.cast_signed());
509    }
510
511    /// Computes the start (eye position) and end positions for a raytrace.
512    pub fn get_ray_endpoints(&self) -> (DVec3, DVec3) {
513        let pos = self.position();
514        let start_pos = DVec3::new(pos.x, self.get_eye_y(), pos.z);
515        let block_interaction_range = self.block_interaction_range();
516        let direction = self.look_angle() * block_interaction_range;
517
518        let end_pos = start_pos + direction;
519        (start_pos, end_pos)
520    }
521
522    /// Returns the player's current game mode.
523    #[must_use]
524    pub fn game_mode(&self) -> GameType {
525        self.game_modes.lock().current()
526    }
527
528    /// Returns the player's previous game mode.
529    #[must_use]
530    pub fn previous_game_mode(&self) -> Option<GameType> {
531        self.game_modes.lock().previous()
532    }
533
534    /// Restores current and previous game mode from persistent player data.
535    pub(crate) fn restore_game_modes(&self, current: GameType, previous: Option<GameType>) {
536        self.game_modes.lock().set_pair(current, previous);
537    }
538
539    /// Changes the current game mode and records the old current mode as previous.
540    fn change_game_mode_state(&self, game_mode: GameType) -> bool {
541        self.game_modes.lock().change_current(game_mode)
542    }
543
544    /// Creates a new player.
545    #[expect(
546        clippy::too_many_arguments,
547        reason = "player construction requires explicit connection, session, world, and identity owners"
548    )]
549    pub fn new(
550        gameprofile: GameProfile,
551        connection: Arc<PlayerConnection>,
552        session: Arc<PlayerSession>,
553        world: Arc<World>,
554        server: Weak<Server>,
555        config: Arc<RuntimeConfig>,
556        entity_id: i32,
557        client_information: ClientInformation,
558    ) -> Self {
559        // Create a single shared inventory container used by both the player and inventory menu
560        let inventory = Arc::new(SyncMutex::new(PlayerInventory::new()));
561        let ender_chest_inventory = Arc::new(SyncMutex::new(PlayerEnderChestContainer::new()));
562
563        let pos = DVec3::new(0.0, 0.0, 0.0);
564
565        let equipment = inventory.clone();
566        let living_base = LivingEntityBase::with_equipment(&vanilla_entities::PLAYER, equipment);
567        let player_uuid = gameprofile.id;
568        let world_ref = Arc::downgrade(&world);
569        Self {
570            gameprofile,
571            connection,
572            session,
573
574            world: ArcSwap::new(world),
575            server,
576            config,
577            base: EntityBase::with_uuid(
578                entity_id,
579                player_uuid,
580                pos,
581                Self::dimensions_for_pose(EntityPose::Standing),
582                world_ref,
583            ),
584            lifecycle: SyncMutex::new(PlayerLifecycleState::default()),
585            movement: SyncMutex::new(MovementState::new()),
586            entity_data: SyncMutex::new({
587                let mut data = PlayerEntityData::new();
588                living_base.initialize_synced_data(&mut data);
589                Self::apply_client_information_to_entity_data(&mut data, &client_information);
590                data
591            }),
592            last_chunk_pos: SyncMutex::new(ChunkPos::new(0, 0)),
593            last_tracking_view: SyncMutex::new(None),
594            client_information: SyncMutex::new(client_information),
595            game_modes: SyncMutex::new(PlayerGameModeState::new(GameType::Survival)),
596            inventory: inventory.clone(),
597            inventory_sync: SyncMutex::new(PlayerInventorySyncState::new()),
598            ender_chest_inventory,
599            last_item_in_main_hand: SyncMutex::new(ItemStack::empty()),
600            inventory_menu: SyncMutex::new(inventory_menu(inventory)),
601            open_menu: SyncMutex::new(player_inventory::OpenMenuState::new()),
602            container_counter: SyncMutex::new(ContainerCounter::new()),
603            teleport_state: SyncMutex::new(TeleportState::new()),
604            item_cooldowns: SyncMutex::new(ItemCooldowns::default()),
605            tick_state: SyncMutex::new(PlayerTickState::new()),
606            sleep_state: SyncMutex::new(PlayerSleepState::new()),
607            respawn_config: SyncMutex::new(None),
608            abilities: SyncMutex::new(Abilities::default()),
609            block_breaking: SyncMutex::new(BlockBreakingManager::new()),
610            living_base,
611            food_data: SyncMutex::new(FoodData::new()),
612            health_sync: SyncMutex::new(HealthSyncState::new()),
613            experience: SyncMutex::new(Experience::default()),
614            permissions: SyncMutex::new(PlayerPermissionState::default()),
615            seen_credits: SyncMutex::new(false),
616            won_game: SyncMutex::new(false),
617            chunk_send_epoch: SyncMutex::new(0),
618            residence: SyncMutex::new(PlayerResidenceState::new()),
619            ender_pearls: SyncMutex::new(Vec::new()),
620            fishing: SyncMutex::new(None),
621            stats: SyncMutex::new(StatsCounter::new()),
622            last_action_time: SyncMutex::new(Instant::now()),
623        }
624    }
625
626    /// Returns the active fishing hook, clearing a stale reference after removal.
627    pub fn fishing_hook(&self) -> Option<Arc<FishingHookEntity>> {
628        let mut fishing = self.fishing.lock();
629        let hook = fishing.as_ref().and_then(Weak::upgrade);
630        if hook.is_none() {
631            *fishing = None;
632        }
633        hook
634    }
635
636    /// Records the hook currently owned by this player.
637    pub fn set_fishing_hook(&self, hook: &Arc<FishingHookEntity>) {
638        *self.fishing.lock() = Some(Arc::downgrade(hook));
639    }
640
641    /// Clears `hook` if it is still this player's active fishing hook.
642    pub fn clear_fishing_hook(&self, hook: &FishingHookEntity) {
643        let mut fishing = self.fishing.lock();
644        if fishing
645            .as_ref()
646            .and_then(Weak::upgrade)
647            .is_some_and(|active| ptr::eq(active.as_ref(), hook))
648        {
649            *fishing = None;
650        }
651    }
652
653    /// Ticks the player.
654    ///
655    /// # Panics
656    ///
657    /// Panics if the player position cannot be restored after `ai_step`. Vanilla treats the
658    /// pre-tick position as authoritative here, so a rejection indicates corrupted entity state.
659    pub fn tick(&self) {
660        self.advance_tick();
661        self.tick_item_cooldowns();
662        self.tick_attack_strength();
663        self.tick_throttlers();
664        self.check_idle_timeout();
665        self.tick_client_load_timeout();
666        self.tick_sleep_counter();
667        if self.is_sleeping() {
668            let world = self.get_world();
669            if !self.bed_rule_value_allows(world.dimension_type.bed_rule.can_sleep) {
670                self.stop_sleep_in_bed(false, true);
671            } else if !self.can_interact_with_level()
672                || self
673                    .sleeping_pos()
674                    .is_none_or(|pos| !world.get_block_state(pos).is_bed())
675            {
676                self.stop_sleep_in_bed(true, true);
677            }
678        }
679
680        self.set_no_physics(self.is_spectator());
681        if self.is_spectator() || self.is_passenger() {
682            self.set_on_ground(false);
683        }
684
685        let tick_position = self.position();
686
687        // Vanilla: ServerGamePacketListenerImpl.resetPosition().
688        self.movement.lock().reset_for_tick(tick_position);
689        self.set_old_position_to_current();
690        self.reset_vehicle_movement_for_tick();
691
692        self.default_tick();
693        self.detect_equipment_updates();
694        self.ai_step();
695
696        // Vanilla snaps the player back to firstGood after ServerPlayer.doTick().
697        if let Err(error) = self.try_set_position(tick_position) {
698            panic!(
699                "failed to restore player {} tick position after ai_step: {error}",
700                self.id()
701            );
702        }
703        self.refresh_fluid_contact();
704
705        self.tick_ack_block_changes();
706
707        if !self.has_client_loaded() {
708            //return;
709        }
710
711        self.living_base.decrement_invulnerable_time();
712        self.tick_mob_effects();
713        self.tick_active_item_use();
714        // TODO: Tick stats even when the player has been dead for more than 20 ticks.
715        self.tick_stats();
716
717        if self.get_health() <= 0.0 {
718            self.tick_death();
719        } else {
720            let world = self.get_world();
721            self.touch_nearby_items();
722            self.block_breaking.lock().tick(self, &world);
723
724            // TODO: Implement remaining player ticking logic here
725            // - Managing game mode specific logic
726            // - Updating advancements
727            // - Handling falling
728
729            self.update_player_attributes();
730            self.living_base.refresh_speed_from_attributes();
731            self.tick_regeneration();
732
733            if self.is_sprinting() && !self.food_data.lock().has_enough_food() {
734                self.set_sprinting(false);
735            }
736        }
737
738        if self.disconnect_if_floating_too_long() {
739            return;
740        }
741        if self.disconnect_if_vehicle_floating_too_long() {
742            return;
743        }
744
745        self.tick_living_state();
746
747        self.tick_open_menu();
748        self.flush_inventory_resync();
749        self.broadcast_inventory_changes();
750        self.update_pose();
751
752        {
753            let health = self.get_health();
754            let (food, saturation) = {
755                let food_data = self.food_data.lock();
756                (food_data.food_level, food_data.saturation_level)
757            };
758
759            let saturation_zero = saturation == 0.0;
760
761            let mut sync = self.health_sync.lock();
762            if sync.needs_update(health, food, saturation_zero) {
763                self.send_packet(CSetHealth {
764                    health,
765                    food,
766                    food_saturation: saturation,
767                });
768                sync.record_sent(health, food, saturation_zero);
769            }
770        }
771
772        self.send_experience_packet_if_dirty();
773
774        self.connection.tick();
775    }
776
777    fn send_experience_packet_if_dirty(&self) {
778        let experience_packet = {
779            let mut experience = self.experience.lock();
780            if experience.dirty {
781                experience.dirty = false;
782                Some(CSetExperience {
783                    progress: experience.progress(),
784                    level: experience.level(),
785                    total_experience: experience.total_points(),
786                })
787            } else {
788                None
789            }
790        };
791        if let Some(packet) = experience_packet {
792            self.send_packet(packet);
793        }
794    }
795
796    /// Ticks the death animation timer.
797    /// Vanilla: `LivingEntity.tickDeath()` (not overridden by `ServerPlayer`).
798    fn tick_death(&self) {
799        let death_time = self.living_base.increment_death_time();
800
801        if death_time >= DEATH_DURATION && !self.is_removed() {
802            let world = self.get_world();
803            let chunk_pos = *self.last_chunk_pos.lock();
804            world.broadcast_to_nearby(
805                chunk_pos,
806                CEntityEvent {
807                    entity_id: self.id(),
808                    event: EntityStatus::Poof,
809                },
810                None,
811            );
812
813            world.unregister_player_entity(self);
814            world.chunk_map.remove_player(self);
815            world.entity_tracker().on_player_leave(self);
816            world.player_area_map.remove_by_entity_id(self.id());
817            self.set_removed(RemovalReason::Killed);
818            assert_eq!(
819                self.remove_all_menus_with_disposition(MenuItemDisposition::Drop),
820                MenuRemovalStatus::Complete,
821                "death removal menu cleanup must run outside a menu callback"
822            );
823        }
824    }
825
826    /// Ticks to award stats that are awarded based on ticks.
827    fn tick_stats(&self) {
828        // These stats are expressed in ticks.
829        // We batch the stats so that the mutex for stats is only
830        // locked once.
831        let mut stats = self.stats.lock();
832        stats.increment(
833            vanilla_stat_types::CUSTOM.get(&vanilla_custom_stats::PLAY_TIME),
834            1,
835        );
836        stats.increment(
837            vanilla_stat_types::CUSTOM.get(&vanilla_custom_stats::TOTAL_WORLD_TIME),
838            1,
839        );
840
841        if Entity::is_alive(self) {
842            stats.increment(
843                vanilla_stat_types::CUSTOM.get(&vanilla_custom_stats::TIME_SINCE_DEATH),
844                1,
845            );
846        }
847        if self.is_discrete() {
848            stats.increment(
849                vanilla_stat_types::CUSTOM.get(&vanilla_custom_stats::SNEAK_TIME),
850                1,
851            );
852        }
853        if !self.is_sleeping() {
854            stats.increment(
855                vanilla_stat_types::CUSTOM.get(&vanilla_custom_stats::TIME_SINCE_REST),
856                1,
857            );
858        }
859    }
860
861    /// Immediately flushes dirty player entity data to tracking players and self.
862    fn sync_entity_data(&self) {
863        if let Some(dirty_values) = self.entity_data.lock().pack_dirty() {
864            let packet = CSetEntityData::new(self.id(), dirty_values);
865            self.get_world()
866                .broadcast_to_entity_trackers(self.id(), packet.clone(), None);
867            self.send_packet(packet);
868        }
869    }
870
871    fn update_dirty_mob_effect_entity_data(&self) {
872        if !self.living_base.take_effects_dirty() {
873            return;
874        }
875
876        let mut display = self.living_base.mob_effect_display_state();
877        if self.game_mode() == GameType::Spectator {
878            display.particles = ParticleList::default();
879            display.invisible = true;
880        }
881
882        {
883            let mut entity_data = self.entity_data.lock();
884            let living = entity_data.living_entity_mut();
885            living.effect_particles.set(display.particles);
886            living.effect_ambience.set(display.ambient);
887        }
888
889        self.entity_data.set_base_invisible_flag(display.invisible);
890        self.entity_data
891            .set_base_glowing_flag(self.has_glowing_tag() || display.glowing);
892    }
893
894    /// Handles a custom payload packet.
895    #[expect(clippy::unused_self, reason = "this is an api function")]
896    pub fn handle_custom_payload(&self, _packet: SCustomPayload) {}
897
898    /// Handles the end of a client tick.
899    pub fn handle_client_tick_end(&self) {
900        self.movement.lock().finish_client_tick();
901    }
902
903    /// Main entry point for dealing damage. Returns `true` if damage was applied.
904    ///
905    /// `world` is vanilla's explicit `ServerLevel` argument and controls
906    /// difficulty scaling and damage gamerules.
907    pub fn hurt(&self, world: &World, source: &DamageSource, amount: f32) -> bool {
908        if LivingEntity::is_invulnerable_to(self, world, source) {
909            return false;
910        }
911
912        {
913            let abilities = self.abilities.lock();
914            if abilities.invulnerable && !source.bypasses_invulnerability() {
915                return false;
916            }
917        }
918
919        // TODO: reset player noActionTime and remove shoulder entities.
920        if self.get_health() <= 0.0 {
921            return false;
922        }
923
924        // Difficulty scaling (vanilla: Player.hurtServer)
925        let mut amount = amount;
926        let causing_entity = source
927            .causing_entity_id
928            .and_then(|entity_id| world.get_entity_by_id(entity_id));
929        if source.scales_with_difficulty(causing_entity.as_deref()) {
930            let difficulty = world.level_data.read().data().difficulty;
931            match difficulty {
932                Difficulty::Peaceful => {
933                    amount = 0.0;
934                }
935                Difficulty::Easy => {
936                    amount = (amount / 2.0 + 1.0).min(amount);
937                }
938                Difficulty::Hard => {
939                    amount = amount * 3.0 / 2.0;
940                }
941                Difficulty::Normal => {}
942            }
943        }
944
945        if amount == 0.0 {
946            return false;
947        }
948
949        LivingEntity::hurt_server(self, world, source, amount)
950    }
951
952    fn disabled_damage_game_rule(source: &DamageSource) -> Option<GameRuleRef<bool>> {
953        if source.is(&vanilla_damage_type_tags::DamageTypeTag::IS_DROWNING) {
954            Some(&DROWNING_DAMAGE)
955        } else if source.is(&vanilla_damage_type_tags::DamageTypeTag::IS_FALL) {
956            Some(&FALL_DAMAGE)
957        } else if source.is(&vanilla_damage_type_tags::DamageTypeTag::IS_FIRE) {
958            Some(&FIRE_DAMAGE)
959        } else if source.is(&vanilla_damage_type_tags::DamageTypeTag::IS_FREEZING) {
960            Some(&FREEZE_DAMAGE)
961        } else {
962            None
963        }
964    }
965
966    /// Applies vanilla player damage reductions and health loss.
967    fn actually_hurt(&self, world: &World, source: &DamageSource, amount: f32) {
968        if LivingEntity::is_invulnerable_to(self, world, source) {
969            return;
970        }
971
972        let damage = LivingEntity::get_damage_after_armor_absorb(self, source, amount);
973        let damage = LivingEntity::get_damage_after_magic_absorb(self, source, damage);
974        let original_damage = damage;
975        let damage = (damage - self.get_absorption_amount()).max(0.0);
976        self.set_absorption_amount(self.get_absorption_amount() - (original_damage - damage));
977
978        let absorbed_damage = original_damage - damage;
979        if (0.0..f32::MAX).contains(&absorbed_damage) {
980            self.award_custom_stat_with_count(
981                &vanilla_custom_stats::DAMAGE_ABSORBED,
982                (absorbed_damage * 10.0).round() as i32,
983            );
984        }
985
986        // TODO: combat tracker (getCombatTracker().recordDamage)
987        if damage != 0.0 {
988            self.cause_food_exhaustion(source.damage_type.exhaustion);
989            self.set_health(self.get_health() - damage);
990            if damage < f32::MAX {
991                self.award_custom_stat_with_count(
992                    &vanilla_custom_stats::DAMAGE_TAKEN,
993                    (damage * 10.0).round() as i32,
994                );
995            }
996            self.game_event(&vanilla_game_events::ENTITY_DAMAGE);
997        }
998    }
999
1000    /// Vanilla: `ServerPlayer.die()` (does NOT call `super.die()`).
1001    fn die(&self, source: &DamageSource) {
1002        if self.is_removed() {
1003            return;
1004        }
1005        if !self.living_base.mark_death_processed() {
1006            return;
1007        }
1008
1009        self.game_event(&vanilla_game_events::ENTITY_DIE);
1010
1011        self.sync_entity_data();
1012
1013        // NOTE: Vanilla `ServerPlayer.die()` does NOT set Pose::Dying — only
1014        // `LivingEntity.die()` does (which ServerPlayer never calls via super).
1015        // The death screen covers the player model, so the pose is irrelevant.
1016
1017        let world = self.get_world();
1018
1019        // Broadcast entity event 3 (death sound) to all nearby players.
1020        let chunk_pos = *self.last_chunk_pos.lock();
1021        world.broadcast_to_nearby(
1022            chunk_pos,
1023            CEntityEvent {
1024                entity_id: self.id(),
1025                event: EntityStatus::Death,
1026            },
1027            None,
1028        );
1029
1030        let show_death_messages = world.get_game_rule(&SHOW_DEATH_MESSAGES);
1031
1032        // TODO: use CombatTracker for multi-arg messages (killer name, item, etc.)
1033        let death_key = format!("death.attack.{}", source.damage_type.message_id);
1034        let death_message = TranslatedMessage {
1035            key: death_key.into(),
1036            fallback: None,
1037            args: Some(Box::new([TextComponent::plain(
1038                self.gameprofile.name.clone(),
1039            )])),
1040        }
1041        .component();
1042
1043        self.send_packet(CPlayerCombatKill {
1044            player_id: self.id(),
1045            message: if show_death_messages {
1046                death_message.clone()
1047            } else {
1048                TextComponent::const_plain("")
1049            },
1050        });
1051
1052        // TODO: team death message visibility (ALWAYS / HIDE_FOR_OTHER_TEAMS / HIDE_FOR_OWN_TEAM)
1053        if show_death_messages {
1054            world.broadcast_system_chat(CSystemChat {
1055                content: death_message,
1056                overlay: false,
1057            });
1058        }
1059
1060        if !world.get_game_rule(&KEEP_INVENTORY) && self.game_mode() != GameType::Spectator {
1061            let drops = self.inventory.lock().take_death_drops();
1062            for item in drops {
1063                let _ = self.drop_item(item, true, false);
1064            }
1065
1066            let reward = self.experience.lock().death_xp_reward();
1067            if reward > 0 {
1068                ExperienceOrbEntity::award(&world, self.position(), reward);
1069            }
1070        }
1071
1072        // TODO: Increment DEATH_COUNT objective criterion
1073        if let Some(killer) = get_kill_credit(self, world.as_ref()) {
1074            self.award_stat(&vanilla_stat_types::ENTITY_KILLED_BY, killer.entity_type());
1075            killer.award_kill_score(self, source);
1076            // TODO: Create wither rose
1077        }
1078
1079        self.award_custom_stat(&vanilla_custom_stats::DEATHS);
1080        self.reset_custom_stat(&vanilla_custom_stats::TIME_SINCE_DEATH);
1081        self.reset_custom_stat(&vanilla_custom_stats::TIME_SINCE_REST);
1082
1083        self.clear_fire();
1084        self.set_ticks_frozen(0);
1085
1086        if world.get_game_rule(&IMMEDIATE_RESPAWN) {
1087            self.respawn();
1088        }
1089    }
1090
1091    /// Returns whether the Player can eat
1092    pub fn can_eat(&self, can_always_eat: bool) -> bool {
1093        let invulnerable = { self.abilities.lock().invulnerable };
1094        let needs_foods = { self.food_data.lock().needs_food() };
1095        invulnerable || can_always_eat || needs_foods
1096    }
1097
1098    /// Cleans up player resources.
1099    #[expect(clippy::unused_self, reason = "this is an api function")]
1100    pub const fn cleanup(&self) {}
1101
1102    /// Returns the world the player is currently in.
1103    pub fn get_world(&self) -> Arc<World> {
1104        self.world.load_full()
1105    }
1106
1107    /// Returns the server this player belongs to.
1108    pub(crate) fn server(&self) -> Arc<Server> {
1109        self.server
1110            .upgrade()
1111            .expect("player must not outlive server")
1112    }
1113
1114    /// Returns the identity of the player's current continuous domain stay.
1115    pub(crate) fn domain_residence_token(&self) -> DomainResidenceToken {
1116        self.residence.lock().token
1117    }
1118
1119    /// Starts a new continuous domain stay and invalidates old restore work.
1120    pub(crate) fn advance_domain_residence(&self) -> DomainResidenceToken {
1121        self.residence.lock().advance()
1122    }
1123
1124    /// Returns whether delayed work still belongs to the current domain stay.
1125    pub(crate) fn is_domain_residence_current(&self, token: DomainResidenceToken) -> bool {
1126        self.residence.lock().token == token
1127    }
1128
1129    /// Installs both persisted restore payloads for a token-owned domain stay.
1130    pub(crate) fn install_pending_domain_restores(
1131        &self,
1132        token: DomainResidenceToken,
1133        world: &World,
1134        root_vehicle: Option<PersistentRootVehicle>,
1135        ender_pearls: Vec<PersistentEnderPearl>,
1136    ) -> bool {
1137        let mut residence = self.residence.lock();
1138        if residence.token != token {
1139            return false;
1140        }
1141
1142        residence.pending_root_vehicle =
1143            root_vehicle.map(|root_vehicle| PendingRootVehicleRestore {
1144                world: world.key.clone(),
1145                root_vehicle,
1146            });
1147        residence.pending_ender_pearls = ender_pearls;
1148        true
1149    }
1150
1151    pub(crate) fn pending_root_vehicle_for_current_world(&self) -> Option<PersistentRootVehicle> {
1152        let world_key = self.get_world().key.clone();
1153        self.residence
1154            .lock()
1155            .pending_root_vehicle
1156            .as_ref()
1157            .filter(|pending| pending.world == world_key)
1158            .map(|pending| pending.root_vehicle.clone())
1159    }
1160
1161    pub(crate) fn take_matching_pending_root_vehicle(
1162        &self,
1163        token: DomainResidenceToken,
1164        world: &World,
1165        attach: [u8; 16],
1166        root_uuid: [u8; 16],
1167    ) -> Option<PersistentRootVehicle> {
1168        let mut residence = self.residence.lock();
1169        if residence.token != token {
1170            return None;
1171        }
1172        let matches = residence
1173            .pending_root_vehicle
1174            .as_ref()
1175            .is_some_and(|pending| {
1176                pending.world == world.key
1177                    && pending.root_vehicle.attach == attach
1178                    && pending.root_vehicle.entity.uuid == root_uuid
1179            });
1180        if matches {
1181            residence
1182                .pending_root_vehicle
1183                .take()
1184                .map(|pending| pending.root_vehicle)
1185        } else {
1186            None
1187        }
1188    }
1189
1190    pub(crate) fn pending_ender_pearls(&self) -> Vec<PersistentEnderPearl> {
1191        self.residence.lock().pending_ender_pearls.clone()
1192    }
1193
1194    pub(crate) fn remove_pending_ender_pearl(&self, uuid: Uuid) {
1195        self.residence
1196            .lock()
1197            .pending_ender_pearls
1198            .retain(|pearl| Uuid::from_bytes(pearl.entity.uuid) != uuid);
1199    }
1200
1201    pub(crate) fn discard_pending_ender_pearl(
1202        &self,
1203        token: DomainResidenceToken,
1204        uuid: Uuid,
1205    ) -> bool {
1206        let mut residence = self.residence.lock();
1207        if residence.token != token {
1208            return false;
1209        }
1210        let old_len = residence.pending_ender_pearls.len();
1211        residence
1212            .pending_ender_pearls
1213            .retain(|pearl| Uuid::from_bytes(pearl.entity.uuid) != uuid);
1214        residence.pending_ender_pearls.len() != old_len
1215    }
1216
1217    pub(crate) fn take_matching_pending_ender_pearl(
1218        &self,
1219        token: DomainResidenceToken,
1220        world: &World,
1221        uuid: Uuid,
1222    ) -> Option<PersistentEnderPearl> {
1223        let mut residence = self.residence.lock();
1224        if residence.token != token {
1225            return None;
1226        }
1227        let world_key = world.key.to_string();
1228        let index = residence.pending_ender_pearls.iter().position(|pearl| {
1229            pearl.world == world_key && Uuid::from_bytes(pearl.entity.uuid) == uuid
1230        })?;
1231        Some(residence.pending_ender_pearls.remove(index))
1232    }
1233
1234    /// Registers a thrown ender pearl so it persists with this player and
1235    /// re-spawns on login (vanilla `ServerPlayer.registerEnderPearl`).
1236    pub fn register_ender_pearl(&self, pearl: &SharedEntity) {
1237        let uuid = pearl.uuid();
1238        let mut pearls = self.ender_pearls.lock();
1239        pearls.retain(|weak| {
1240            weak.upgrade()
1241                .is_some_and(|p| !p.is_removed() && p.uuid() != uuid)
1242        });
1243        pearls.push(Arc::downgrade(pearl));
1244        drop(pearls);
1245        self.remove_pending_ender_pearl(uuid);
1246    }
1247
1248    /// Deregisters a thrown ender pearl once it hits, teleports, or is discarded
1249    /// (vanilla `ServerPlayer.deregisterEnderPearl`).
1250    pub fn deregister_ender_pearl(&self, uuid: Uuid) {
1251        self.ender_pearls
1252            .lock()
1253            .retain(|weak| weak.upgrade().is_some_and(|p| p.uuid() != uuid));
1254    }
1255
1256    /// Returns this player's live, in-flight ender pearls, pruning dead entries.
1257    #[must_use]
1258    pub fn ender_pearls(&self) -> Vec<SharedEntity> {
1259        let mut pearls = self.ender_pearls.lock();
1260        pearls.retain(|weak| weak.upgrade().is_some_and(|p| !p.is_removed()));
1261        pearls.iter().filter_map(Weak::upgrade).collect()
1262    }
1263
1264    /// Rebinds live pearls to a fresh respawn incarnation with the same player UUID.
1265    pub(crate) fn rebind_ender_pearls_to(&self, replacement: &Arc<Self>) {
1266        debug_assert_eq!(self.gameprofile.id, replacement.gameprofile.id);
1267        let replacement_entity: SharedEntity = replacement.clone();
1268        for pearl in self.ender_pearls() {
1269            if pearl.projectile_owner_uuid() == Some(self.gameprofile.id) {
1270                pearl.restore_owner_reference(&replacement_entity);
1271            }
1272        }
1273    }
1274
1275    /// Appends vanilla-shaped player state used by command NBT predicates.
1276    pub(crate) fn save_command_nbt(&self, nbt: &mut NbtCompound) {
1277        {
1278            let inventory = self.inventory.lock();
1279            nbt.insert("Inventory", inventory.to_vanilla_inventory_nbt());
1280            nbt.insert("SelectedItemSlot", i32::from(inventory.get_selected_slot()));
1281        }
1282
1283        {
1284            let experience = self.experience.lock();
1285            nbt.insert("XpP", experience.progress());
1286            nbt.insert("XpLevel", experience.level());
1287            nbt.insert("XpTotal", experience.total_points());
1288        }
1289        nbt.insert("Score", self.score());
1290
1291        {
1292            let food = self.food_data.lock();
1293            nbt.insert("foodLevel", food.food_level);
1294            nbt.insert("foodTickTimer", food.tick_timer);
1295            nbt.insert("foodSaturationLevel", food.saturation_level);
1296            nbt.insert("foodExhaustionLevel", food.exhaustion_level);
1297        }
1298
1299        {
1300            let abilities = self.abilities.lock();
1301            let mut abilities_nbt = NbtCompound::new();
1302            abilities_nbt.insert(
1303                "invulnerable",
1304                NbtTag::Byte(i8::from(abilities.invulnerable)),
1305            );
1306            abilities_nbt.insert("flying", NbtTag::Byte(i8::from(abilities.flying)));
1307            abilities_nbt.insert("mayfly", NbtTag::Byte(i8::from(abilities.may_fly)));
1308            abilities_nbt.insert("instabuild", NbtTag::Byte(i8::from(abilities.instabuild)));
1309            abilities_nbt.insert("mayBuild", NbtTag::Byte(i8::from(abilities.may_build)));
1310            abilities_nbt.insert("flySpeed", abilities.flying_speed);
1311            abilities_nbt.insert("walkSpeed", abilities.walking_speed);
1312            nbt.insert("abilities", NbtTag::Compound(abilities_nbt));
1313        }
1314
1315        nbt.insert("playerGameType", self.game_mode() as i32);
1316        if let Some(previous_game_mode) = self.previous_game_mode() {
1317            nbt.insert("previousPlayerGameType", previous_game_mode as i32);
1318        }
1319        nbt.insert(
1320            "seenCredits",
1321            NbtTag::Byte(i8::from(self.has_seen_credits())),
1322        );
1323        nbt.insert("Dimension", self.get_world().key.to_string());
1324
1325        if let Some(vehicle) = self.vehicle()
1326            && let Some(root_vehicle) = self.root_vehicle()
1327            && root_vehicle.id() != self.id()
1328            && root_vehicle.has_exactly_one_player_passenger()
1329            && let Some(entity_nbt) = root_vehicle.nbt_for_passenger_save()
1330        {
1331            let mut root_vehicle_nbt = NbtCompound::new();
1332            root_vehicle_nbt.insert(
1333                "Attach",
1334                NbtTag::IntArray(vehicle.uuid().to_int_array().to_vec()),
1335            );
1336            root_vehicle_nbt.insert("Entity", NbtTag::Compound(entity_nbt));
1337            nbt.insert("RootVehicle", NbtTag::Compound(root_vehicle_nbt));
1338        }
1339
1340        let ender_pearls = self
1341            .ender_pearls()
1342            .into_iter()
1343            .filter_map(|pearl| {
1344                let world = pearl.level()?;
1345                let mut pearl_nbt = pearl.nbt_for_passenger_save()?;
1346                pearl_nbt.insert("ender_pearl_dimension", world.key.to_string());
1347                Some(pearl_nbt)
1348            })
1349            .collect::<Vec<_>>();
1350        if !ender_pearls.is_empty() {
1351            nbt.insert("ender_pearls", NbtList::Compound(ender_pearls));
1352        }
1353    }
1354
1355    /// Marks live ender pearls as stored with this player so chunk saves remove
1356    /// them from world storage and player data remains the sole owner.
1357    pub fn store_ender_pearls_with_player(&self) {
1358        for pearl in self.ender_pearls() {
1359            let world = pearl.level();
1360            let chunk = ChunkPos::from_entity_pos(pearl.position());
1361            pearl.set_removed(RemovalReason::StoredWithPlayer);
1362            if let Some(world) = world {
1363                world.mark_chunk_dirty(chunk);
1364            }
1365        }
1366    }
1367
1368    /// Returns this player's local server tick count.
1369    #[must_use]
1370    pub fn tick_count(&self) -> i32 {
1371        self.tick_state.lock().tick_count()
1372    }
1373
1374    /// Returns vanilla `Player.takeXpDelay`.
1375    #[must_use]
1376    pub(crate) fn take_xp_delay(&self) -> i32 {
1377        self.tick_state.lock().take_xp_delay()
1378    }
1379
1380    /// Sets vanilla `Player.takeXpDelay`.
1381    pub(crate) fn set_take_xp_delay(&self, delay: i32) {
1382        self.tick_state.lock().set_take_xp_delay(delay);
1383    }
1384
1385    fn primary_step_sound_block_pos(&self, affecting_pos: BlockPos) -> BlockPos {
1386        let above_pos = affecting_pos.above();
1387        let above_state = self.get_world().get_block_state(above_pos);
1388        let above_block = above_state.get_block();
1389
1390        if above_block.has_tag(&BlockTag::INSIDE_STEP_SOUND_BLOCKS)
1391            || above_block.has_tag(&BlockTag::COMBINATION_STEP_SOUND_BLOCKS)
1392        {
1393            above_pos
1394        } else {
1395            affecting_pos
1396        }
1397    }
1398
1399    /// Resets the last action time of the player (to the current time).
1400    pub fn reset_last_action_time(&self) {
1401        *self.last_action_time.lock() = Instant::now();
1402    }
1403
1404    fn check_idle_timeout(&self) {
1405        if let Some(server) = self.server.upgrade() {
1406            let player_idle_timeout = server.player_idle_timeout.load(Ordering::Relaxed);
1407            if player_idle_timeout > 0
1408                && Instant::now().duration_since(*self.last_action_time.lock())
1409                    > Duration::from_mins(player_idle_timeout as u64)
1410                && !self.has_won_game()
1411            {
1412                self.disconnect(translations::MULTIPLAYER_DISCONNECT_IDLING.msg());
1413            }
1414        }
1415    }
1416}
1417
1418impl Entity for Player {
1419    fn base(&self) -> &EntityBase {
1420        &self.base
1421    }
1422
1423    fn entity_type(&self) -> EntityTypeRef {
1424        &vanilla_entities::PLAYER
1425    }
1426
1427    fn base_tick(&self) {
1428        LivingEntity::base_tick_living_entity(self);
1429    }
1430
1431    fn scoreboard_name(&self) -> String {
1432        self.gameprofile.name.clone()
1433    }
1434
1435    fn name(&self) -> TextComponent {
1436        TextComponent::plain(self.gameprofile.name.clone())
1437    }
1438
1439    fn display_name(&self) -> TextComponent {
1440        self.name()
1441            .click_event(ClickEvent::suggest_command(format!(
1442                "/tell {} ",
1443                self.gameprofile.name
1444            )))
1445            .hover_event(HoverEvent::show_entity(
1446                "minecraft:player",
1447                self.uuid(),
1448                Some(self.name()),
1449            ))
1450            .insertion(self.gameprofile.name.clone())
1451    }
1452
1453    fn plain_text_name(&self) -> String {
1454        self.gameprofile.name.clone()
1455    }
1456
1457    fn look_at(&self, from_anchor: EntityAnchor, target: DVec3) {
1458        apply_entity_look_at(self, from_anchor, target);
1459        self.send_packet(CPlayerLookAt::position(
1460            protocol_look_at_anchor(from_anchor),
1461            target,
1462        ));
1463    }
1464
1465    fn look_at_entity(
1466        &self,
1467        from_anchor: EntityAnchor,
1468        target: &dyn Entity,
1469        target_anchor: EntityAnchor,
1470    ) {
1471        let target_position = target_anchor.position(target);
1472        apply_entity_look_at(self, from_anchor, target_position);
1473        self.send_packet(CPlayerLookAt::entity(
1474            protocol_look_at_anchor(from_anchor),
1475            target_position,
1476            target.id(),
1477            protocol_look_at_anchor(target_anchor),
1478        ));
1479    }
1480
1481    fn is_always_ticking(&self) -> bool {
1482        true
1483    }
1484
1485    fn update_swimming(&self) {
1486        if self.is_flying() {
1487            self.set_shared_swimming(false);
1488        } else {
1489            self.default_update_swimming();
1490        }
1491    }
1492
1493    fn ride_tick(&self) {
1494        let pre = self.position();
1495        if self.wants_to_stop_riding() && self.is_passenger() {
1496            self.stop_riding();
1497        } else {
1498            self.default_ride_tick();
1499            self.reset_fall_distance();
1500        }
1501        self.check_riding_statistics(self.position() - pre);
1502    }
1503
1504    fn stop_riding(&self) {
1505        let old_vehicle = self.vehicle();
1506        self.base().stop_riding();
1507        self.base.set_boarding_cooldown(0);
1508        let Some(old_vehicle) = old_vehicle else {
1509            return;
1510        };
1511
1512        self.remove_active_effects_for_vehicle(old_vehicle.as_ref());
1513        self.send_packet(CSetPassengers::new(
1514            old_vehicle.id(),
1515            Self::passenger_ids_for_packet(old_vehicle.as_ref()),
1516        ));
1517    }
1518
1519    fn teleport_to(&self, pos: DVec3) -> Result<(), EntityMoveError> {
1520        let (yaw, pitch) = self.rotation();
1521        self.teleport(pos, yaw, pitch)
1522    }
1523
1524    fn start_riding(&self, entity_to_ride: &SharedEntity) -> bool {
1525        let Some(world) = self.level() else {
1526            return false;
1527        };
1528        let Some(passenger) = world.get_entity_by_id(self.id()) else {
1529            return false;
1530        };
1531        if !start_riding_entities(&passenger, entity_to_ride) {
1532            return false;
1533        }
1534
1535        entity_to_ride.position_rider(self.as_entity_event_source());
1536        let position = self.position();
1537        let (yaw, pitch) = self.rotation();
1538        if let Err(error) = self.teleport(position, yaw, pitch) {
1539            panic!(
1540                "failed to synchronize player {} mounted position: {error}",
1541                self.id()
1542            );
1543        }
1544        self.send_active_effects_for_vehicle(entity_to_ride.as_ref());
1545        self.send_packet(CSetPassengers::new(
1546            entity_to_ride.id(),
1547            Self::passenger_ids_for_packet(entity_to_ride.as_ref()),
1548        ));
1549        true
1550    }
1551
1552    fn broadcast_to_player(&self, player: &Player) -> bool {
1553        if player.is_spectator() {
1554            true
1555        } else {
1556            !self.is_spectator()
1557        }
1558    }
1559
1560    fn fall_sounds(&self) -> (SoundEventRef, SoundEventRef) {
1561        (
1562            &sound_events::ENTITY_PLAYER_SMALL_FALL,
1563            &sound_events::ENTITY_PLAYER_BIG_FALL,
1564        )
1565    }
1566
1567    fn is_alive(&self) -> bool {
1568        !self.is_removed() && self.get_health() > 0.0
1569    }
1570
1571    fn blocks_building(&self) -> bool {
1572        true
1573    }
1574
1575    fn is_pickable(&self) -> bool {
1576        !self.is_spectator() && !self.is_removed()
1577    }
1578
1579    fn is_pushable(&self) -> bool {
1580        self.get_health() > 0.0 && !self.is_spectator() && !self.on_climbable()
1581    }
1582
1583    fn on_climbable(&self) -> bool {
1584        Player::on_climbable(self)
1585    }
1586
1587    fn is_spectator(&self) -> bool {
1588        self.game_mode() == GameType::Spectator
1589    }
1590
1591    fn is_flying_player(&self) -> bool {
1592        self.is_flying()
1593    }
1594
1595    fn fire_immune_ticks(&self) -> i32 {
1596        20
1597    }
1598
1599    fn remaining_fire_ticks_cap(&self) -> Option<i32> {
1600        self.abilities.lock().invulnerable.then_some(1)
1601    }
1602
1603    fn get_default_gravity(&self) -> f64 {
1604        LivingEntity::get_attribute_gravity(self)
1605    }
1606
1607    fn fire_ignite_extra_ticks(&self) -> i32 {
1608        rand::random_range(1..=2)
1609    }
1610
1611    fn can_freeze(&self) -> bool {
1612        if self.is_spectator() {
1613            return false;
1614        }
1615
1616        self.default_living_can_freeze()
1617    }
1618
1619    fn make_stuck_in_block(&self, state: BlockStateId, speed_multiplier: DVec3) {
1620        if !self.is_flying() {
1621            self.default_make_stuck_in_block(state, speed_multiplier);
1622        }
1623
1624        // TODO: Reset current impulse context once vehicle/player impulse contexts exist.
1625    }
1626
1627    fn can_be_hit_by_projectile(&self) -> bool {
1628        self.get_health() > 0.0 && self.is_pickable()
1629    }
1630
1631    fn uses_client_movement_packets(&self) -> bool {
1632        true
1633    }
1634
1635    fn can_simulate_movement(&self) -> bool {
1636        true
1637    }
1638
1639    fn is_effective_ai(&self) -> bool {
1640        true
1641    }
1642
1643    fn known_movement(&self) -> DVec3 {
1644        if let Some(vehicle) = self.vehicle()
1645            && vehicle
1646                .controlling_passenger()
1647                .is_none_or(|controller| controller.id() != self.id())
1648        {
1649            return vehicle.known_movement();
1650        }
1651
1652        self.movement.lock().last_known_client_movement()
1653    }
1654
1655    fn known_speed(&self) -> DVec3 {
1656        if let Some(vehicle) = self.vehicle()
1657            && vehicle
1658                .controlling_passenger()
1659                .is_none_or(|controller| controller.id() != self.id())
1660        {
1661            return vehicle.known_speed();
1662        }
1663
1664        self.movement.lock().last_known_client_movement()
1665    }
1666
1667    fn is_suppressing_bounce(&self) -> bool {
1668        self.is_crouching()
1669    }
1670
1671    fn cause_fall_damage(
1672        &self,
1673        fall_distance: f64,
1674        damage_modifier: f32,
1675        source: &DamageSource,
1676    ) -> bool {
1677        if self.abilities.lock().may_fly {
1678            return false;
1679        }
1680
1681        if fall_distance >= 2.0 {
1682            self.award_custom_stat_with_count(
1683                &vanilla_custom_stats::FALL_ONE_CM,
1684                (fall_distance * 100.0).round() as i32,
1685            );
1686        }
1687
1688        LivingEntity::cause_living_fall_damage(self, fall_distance, damage_modifier, source)
1689    }
1690
1691    fn synced_data(&self) -> Option<&dyn EntitySyncedData> {
1692        Some(&self.entity_data)
1693    }
1694
1695    fn update_data_before_sync(&self) {
1696        self.update_dirty_mob_effect_entity_data();
1697    }
1698
1699    fn max_up_step(&self) -> f32 {
1700        self.attributes()
1701            .lock()
1702            .get_value(vanilla_attributes::STEP_HEIGHT)
1703            .unwrap_or(0.6) as f32
1704    }
1705
1706    fn backs_off_from_edge(&self) -> bool {
1707        self.is_crouching() && !self.is_flying()
1708    }
1709
1710    fn is_pushed_by_fluid(&self) -> bool {
1711        !self.is_flying()
1712    }
1713
1714    fn is_crouching(&self) -> bool {
1715        Player::is_crouching(self)
1716    }
1717
1718    fn may_interact(&self, world: &World, pos: BlockPos) -> bool {
1719        world.may_interact(self, pos)
1720    }
1721
1722    fn is_swimming(&self) -> bool {
1723        Player::is_swimming(self)
1724    }
1725
1726    fn sound_source(&self) -> SoundSource {
1727        SoundSource::Players
1728    }
1729
1730    /// Matches vanilla `Player.playSound`, which excludes the source player.
1731    fn play_sound(&self, sound: SoundEventRef, volume: f32, pitch: f32) {
1732        if let Some(world) = self.level() {
1733            world.play_sound_at(
1734                sound,
1735                self.sound_source(),
1736                self.position(),
1737                volume,
1738                pitch,
1739                Some(self.id()),
1740            );
1741        }
1742    }
1743
1744    fn swim_sound(&self) -> SoundEventRef {
1745        &sound_events::ENTITY_PLAYER_SWIM
1746    }
1747
1748    fn play_step_sound(&self, on_pos: BlockPos, on_state: BlockStateId) {
1749        if self.is_in_water() {
1750            self.water_swim_sound();
1751            self.play_muffled_step_sound(on_state);
1752            return;
1753        }
1754
1755        let primary_step_sound_pos = self.primary_step_sound_block_pos(on_pos);
1756        if primary_step_sound_pos == on_pos {
1757            self.play_block_step_sound(on_state);
1758        } else {
1759            let primary_state = self.get_world().get_block_state(primary_step_sound_pos);
1760            if primary_state
1761                .get_block()
1762                .has_tag(&BlockTag::COMBINATION_STEP_SOUND_BLOCKS)
1763            {
1764                self.play_combination_step_sounds(primary_state, on_state);
1765            } else {
1766                self.play_block_step_sound(primary_state);
1767            }
1768        }
1769    }
1770
1771    fn movement_emission(&self) -> EntityMovementEmission {
1772        if self.is_flying() || self.on_ground() && self.is_discrete() {
1773            EntityMovementEmission::None
1774        } else {
1775            EntityMovementEmission::All
1776        }
1777    }
1778
1779    fn on_below_world(&self) {
1780        let world = self.get_world();
1781        self.hurt(
1782            &world,
1783            &DamageSource::environment(&vanilla_damage_types::OUT_OF_WORLD),
1784            4.0,
1785        );
1786    }
1787
1788    fn dimensions_for_pose(&self, pose: EntityPose) -> EntityDimensions {
1789        let dimensions = Player::dimensions_for_pose(pose);
1790        if pose == EntityPose::Sleeping || self.entity_type().fixed {
1791            dimensions
1792        } else {
1793            dimensions.scale(LivingEntity::get_scale(self))
1794        }
1795    }
1796
1797    fn hurt(&self, world: &World, source: &DamageSource, amount: f32) -> bool {
1798        // Delegates to Player's inherent hurt method which handles
1799        // player-specific prechecks before the shared living hurt path.
1800        Player::hurt(self, world, source, amount)
1801    }
1802
1803    fn killed_entity(
1804        &self,
1805        _world: &World,
1806        entity: &dyn LivingEntity,
1807        _source: &DamageSource,
1808    ) -> bool {
1809        log::debug!("1");
1810        self.award_stat(&vanilla_stat_types::ENTITY_KILLED, entity.entity_type());
1811        true
1812    }
1813
1814    fn award_kill_score(&self, victim: &dyn Entity, _killing_blow: &DamageSource) {
1815        if self.id() != victim.id() {
1816            // TODO: Trigger advancement criteria.
1817            // TODO: Increment the score of some objectives of this player.
1818            self.award_custom_stat(if victim.as_player().is_some() {
1819                &vanilla_custom_stats::PLAYER_KILLS
1820            } else {
1821                &vanilla_custom_stats::MOB_KILLS
1822            });
1823            // TODO: Handle team kill
1824        }
1825    }
1826}
1827
1828const fn protocol_look_at_anchor(anchor: EntityAnchor) -> LookAtAnchor {
1829    match anchor {
1830        EntityAnchor::Feet => LookAtAnchor::Feet,
1831        EntityAnchor::Eyes => LookAtAnchor::Eyes,
1832    }
1833}
1834
1835impl LivingEntity for Player {
1836    fn living_synced_data(&self) -> Option<&dyn LivingEntitySyncedData> {
1837        Some(&self.entity_data)
1838    }
1839
1840    fn tick_living_entity(&self) {
1841        Player::tick(self);
1842    }
1843
1844    fn get_health(&self) -> f32 {
1845        *self.entity_data.lock().living_entity().health.get()
1846    }
1847
1848    fn set_health(&self, health: f32) {
1849        let max_health = self.get_max_health();
1850        let clamped = health.clamp(0.0, max_health);
1851        self.entity_data
1852            .lock()
1853            .living_entity_mut()
1854            .health
1855            .set(clamped);
1856    }
1857
1858    fn living_base(&self) -> &LivingEntityBase {
1859        &self.living_base
1860    }
1861
1862    fn is_using_item(&self) -> bool {
1863        self.living_base.is_using_item()
1864    }
1865
1866    fn get_luck(&self) -> f32 {
1867        self.attributes()
1868            .lock()
1869            .required_value(vanilla_attributes::LUCK) as f32
1870    }
1871
1872    fn can_be_seen_as_enemy(&self) -> bool {
1873        !self.abilities.lock().invulnerable
1874            && !self.is_invulnerable()
1875            && self.can_be_seen_by_anyone()
1876    }
1877
1878    fn is_invulnerable_to(&self, world: &World, source: &DamageSource) -> bool {
1879        if self.default_is_invulnerable_to(source)
1880            || enchantment_helper::is_immune_to_damage(world, self, source)
1881        {
1882            return true;
1883        }
1884
1885        if let Some(rule) = Self::disabled_damage_game_rule(source) {
1886            return !world.get_game_rule(rule);
1887        }
1888
1889        !self.has_client_loaded()
1890    }
1891
1892    fn hurt_armor(&self, source: &DamageSource, damage: f32) {
1893        self.do_hurt_equipment(
1894            source,
1895            damage,
1896            &[
1897                EquipmentSlot::Feet,
1898                EquipmentSlot::Legs,
1899                EquipmentSlot::Chest,
1900                EquipmentSlot::Head,
1901            ],
1902        );
1903    }
1904
1905    fn actually_hurt(&self, world: &World, source: &DamageSource, amount: f32) {
1906        Player::actually_hurt(self, world, source, amount);
1907    }
1908
1909    fn hurt_broadcast_chunk(&self) -> ChunkPos {
1910        *self.last_chunk_pos.lock()
1911    }
1912
1913    fn die(&self, source: &DamageSource) {
1914        Player::die(self, source);
1915    }
1916
1917    fn with_equipment_slot(&self, slot: EquipmentSlot, visitor: &mut dyn FnMut(&ItemStack)) {
1918        let inventory = self.inventory.lock();
1919        visitor(inventory.get_ref(slot));
1920    }
1921
1922    fn with_equipment_slot_mut(
1923        &self,
1924        slot: EquipmentSlot,
1925        visitor: &mut dyn FnMut(&mut ItemStack),
1926    ) {
1927        let mut inventory = self.inventory.lock();
1928        inventory.with_equipment_item_mut(slot, visitor);
1929    }
1930
1931    fn interact_living_entity_with_equippable(
1932        &self,
1933        player: &Player,
1934        hand: InteractionHand,
1935    ) -> InteractionResult {
1936        let item_stack = {
1937            let inventory = player.inventory.lock();
1938            let item_stack = inventory.get_item_in_hand(hand);
1939            item_stack.clone()
1940        };
1941        let Some(equippable) = item_stack.get_equippable() else {
1942            return InteractionResult::Pass;
1943        };
1944        if !equippable.equip_on_interact {
1945            return InteractionResult::Pass;
1946        }
1947
1948        let slot = equippable.slot;
1949        let can_equip = |stack: &ItemStack| {
1950            stack.get_equippable().is_some_and(|equippable| {
1951                equippable.equip_on_interact
1952                    && equippable.slot == slot
1953                    && self.is_equippable_in_slot(stack, slot)
1954            })
1955        };
1956        if !can_equip(&item_stack) || !Entity::is_alive(self) {
1957            return InteractionResult::Pass;
1958        }
1959
1960        let source_ref = ContainerRef::from(player.inventory.clone());
1961        let target_ref = ContainerRef::from(self.inventory.clone());
1962        let source_id = source_ref.container_id();
1963        let target_id = target_ref.container_id();
1964        let mut guard = ContainerLockGuard::lock_all(&[source_ref, target_ref]);
1965        let source_slot = match hand {
1966            InteractionHand::MainHand => EquipmentSlot::MainHand,
1967            InteractionHand::OffHand => EquipmentSlot::OffHand,
1968        };
1969
1970        let equipped = if source_id == target_id {
1971            let Some(inventory) = guard.get_typed_mut::<PlayerInventory>(source_id) else {
1972                unreachable!("player inventory container retains its concrete type");
1973            };
1974            if !can_equip(inventory.get_item_in_hand(hand)) || !inventory.get_ref(slot).is_empty() {
1975                return InteractionResult::Pass;
1976            }
1977
1978            let equipped = inventory.get_mut(source_slot).split(1);
1979            if equipped.is_empty() {
1980                return InteractionResult::Pass;
1981            }
1982            let equipped_for_effects = equipped.copy_with_count(1);
1983            *inventory.get_mut(slot) = equipped;
1984            equipped_for_effects
1985        } else {
1986            let Some((source_inventory, target_inventory)) =
1987                guard.get_two_typed_mut::<PlayerInventory, PlayerInventory>(source_id, target_id)
1988            else {
1989                unreachable!("player inventory containers retain their concrete type");
1990            };
1991            if !can_equip(source_inventory.get_item_in_hand(hand))
1992                || !target_inventory.get_ref(slot).is_empty()
1993            {
1994                return InteractionResult::Pass;
1995            }
1996
1997            let equipped = source_inventory.get_mut(source_slot).split(1);
1998            if equipped.is_empty() {
1999                return InteractionResult::Pass;
2000            }
2001            let equipped_for_effects = equipped.copy_with_count(1);
2002            *target_inventory.get_mut(slot) = equipped;
2003            equipped_for_effects
2004        };
2005        drop(guard);
2006
2007        player.inventory.lock().set_changed();
2008        if source_id != target_id {
2009            self.inventory.lock().set_changed();
2010        }
2011
2012        if let Some(sound) = self.equip_sound(slot, &equipped) {
2013            self.play_sound(sound, 1.0, 1.0);
2014        }
2015        // TODO: Emit EQUIP game event once game-event dispatch is implemented.
2016        InteractionResult::Success
2017    }
2018
2019    fn has_infinite_materials(&self) -> bool {
2020        Player::has_infinite_materials(self)
2021    }
2022
2023    fn handle_extra_items_created_on_use(&self, extra: ItemStack) {
2024        let leftover = self.inventory.lock().add_or_return(extra);
2025        if !leftover.is_empty() {
2026            let _ = self.drop_item(leftover, false, false);
2027        }
2028    }
2029
2030    fn get_absorption_amount(&self) -> f32 {
2031        *self.entity_data.lock().player_absorption.get()
2032    }
2033
2034    fn set_absorption_amount(&self, amount: f32) {
2035        let max_absorption = self
2036            .living_base
2037            .attributes()
2038            .lock()
2039            .required_value(vanilla_attributes::MAX_ABSORPTION) as f32;
2040        self.entity_data
2041            .lock()
2042            .player_absorption
2043            .set(amount.clamp(0.0, max_absorption));
2044    }
2045
2046    fn is_affected_by_fluids(&self) -> bool {
2047        !self.is_flying()
2048    }
2049
2050    fn can_glide(&self) -> bool {
2051        !self.is_flying() && self.default_can_glide()
2052    }
2053
2054    fn is_immobile(&self) -> bool {
2055        self.default_is_immobile() || self.is_sleeping()
2056    }
2057
2058    fn stop_sleeping(&self) {
2059        self.stop_sleep_in_bed(true, true);
2060    }
2061
2062    fn jump_from_ground(&self) {
2063        self.default_jump_from_ground();
2064        self.award_custom_stat(&vanilla_custom_stats::JUMP);
2065        if self.is_sprinting() {
2066            self.cause_food_exhaustion(food_constants::EXHAUSTION_SPRINT_JUMP);
2067        } else {
2068            self.cause_food_exhaustion(food_constants::EXHAUSTION_JUMP);
2069        }
2070    }
2071
2072    fn ai_step(&self) -> Option<MoveResult> {
2073        if self.is_flying() && !self.is_passenger() {
2074            self.reset_fall_distance();
2075        }
2076
2077        let result = self.default_ai_step();
2078        self.set_y_head_rot(self.rotation().0);
2079        result
2080    }
2081
2082    fn travel(&self, input: DVec3) -> Option<MoveResult> {
2083        if self.is_passenger() {
2084            return self.default_travel(input);
2085        }
2086
2087        if self.is_swimming() {
2088            let look_angle_y = self.look_angle().y;
2089            let multiplier = if look_angle_y < -0.2 { 0.085 } else { 0.06 };
2090            let has_fluid_above = self.level().is_some_and(|world| {
2091                let position = self.position();
2092                let pos = BlockPos::containing(position.x, position.y + 0.9, position.z);
2093                !get_fluid_state(&world, pos).is_empty()
2094            });
2095            if look_angle_y <= 0.0 || self.is_jumping() || has_fluid_above {
2096                let velocity = self.velocity();
2097                self.set_velocity(
2098                    velocity + DVec3::new(0.0, (look_angle_y - velocity.y) * multiplier, 0.0),
2099                );
2100            }
2101        }
2102
2103        if self.is_flying() {
2104            let original_movement_y = self.velocity().y;
2105            let result = self.default_travel(input);
2106            let velocity = self.velocity();
2107            self.set_velocity(DVec3::new(
2108                velocity.x,
2109                original_movement_y * 0.6,
2110                velocity.z,
2111            ));
2112            result
2113        } else {
2114            self.default_travel(input)
2115        }
2116    }
2117
2118    fn get_flying_speed(&self) -> f32 {
2119        if self.is_flying() && !self.is_passenger() {
2120            let flying_speed = self.abilities.lock().flying_speed;
2121            if self.is_sprinting() {
2122                flying_speed * 2.0
2123            } else {
2124                flying_speed
2125            }
2126        } else if self.is_sprinting() {
2127            0.025_999_999
2128        } else {
2129            0.02
2130        }
2131    }
2132
2133    fn on_equipped_item_broken(&self, item: ItemRef, slot: EquipmentSlot) {
2134        self.broadcast_entity_event(slot.into());
2135        self.refresh_equipment_attribute_modifiers(slot);
2136        self.award_stat(&vanilla_stat_types::ITEM_BROKEN, item);
2137    }
2138}
2139
2140impl TextResolutor for Player {
2141    fn resolve_content(&self, _resolvable: &Resolvable) -> TextComponent {
2142        TextComponent::new()
2143    }
2144
2145    fn resolve_custom(&self, _data: &CustomData) -> Option<TextComponent> {
2146        None
2147    }
2148
2149    fn translate(&self, _key: &str) -> Option<String> {
2150        None
2151    }
2152}
2153
2154#[cfg(test)]
2155mod tests;