Skip to main content

steel_core/player/
mod.rs

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