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 tick_state;
24
25pub use abilities::{Abilities, DEFAULT_FLYING_SPEED};
26use chat::ChatState;
27pub use chat::{LastSeen, LastSeenMessagesValidator, MessageCache};
28use connection::NetworkConnection as _;
29pub use connection::{ClientInformation, PlayerConnection};
30use container_counter::ContainerCounter;
31use food_data::FoodData;
32use game_mode::{BlockBreakingManager, PlayerGameModeState};
33use glam::DVec3;
34use health_sync::HealthSyncState;
35use item_cooldowns::ItemCooldowns;
36use lifecycle::PlayerLifecycleState;
37pub(crate) use lifecycle::ResetReason;
38pub use movement::PlayerInput;
39use movement::{MovementState, TeleportState};
40use permissions::PlayerPermissionState;
41pub(crate) use profile::{GAME_PROFILE_CACHE_LIMIT, KnownPlayerNameLookup, lookup_online_profile};
42pub use profile::{
43    GameProfile, GameProfileAction, KnownPlayer, KnownPlayers, ProfileLookupError,
44    is_valid_player_name, offline_uuid,
45};
46use simdnbt::owned::{NbtCompound, NbtList, NbtTag};
47use std::sync::{Arc, Weak};
48use steel_protocol::packets::game::{
49    CEntityEvent, CPlayerCombatKill, CPlayerLookAt, CRespawn, CSetDefaultSpawnPosition, CSetHealth,
50    CSetHeldSlot, CSetPassengers, ClientCommandAction, LookAtAnchor, RelativeMovement, SoundSource,
51};
52use steel_protocol::packets::game::{CLevelEvent, CSetEntityData, CSetExperience};
53use steel_registry::blocks::block_state_ext::BlockStateExt as _;
54use steel_registry::entity_data::{EntityPose, ParticleList};
55use steel_registry::entity_type::{EntityDimensions, EntityTypeRef};
56use steel_registry::game_rules::GameRuleRef;
57use steel_registry::sound_event::SoundEventRef;
58use steel_registry::vanilla_block_tags::BlockTag;
59use steel_registry::vanilla_entity_data::PlayerEntityData;
60use steel_registry::vanilla_game_rules::{
61    DROWNING_DAMAGE, FALL_DAMAGE, FIRE_DAMAGE, FREEZE_DAMAGE, IMMEDIATE_RESPAWN, KEEP_INVENTORY,
62    SHOW_DEATH_MESSAGES,
63};
64use steel_registry::{
65    level_events, sound_events, vanilla_attributes, vanilla_damage_type_tags, vanilla_entities,
66    vanilla_game_events,
67};
68use steel_utils::{entity_events::EntityStatus, locks::Shared};
69use tick_state::PlayerTickState;
70use uuid::Uuid;
71
72use arc_swap::ArcSwap;
73use steel_utils::locks::SyncMutex;
74use steel_utils::types::{Difficulty, GameType, InteractionHand};
75use text_components::resolving::TextResolutor;
76use text_components::translation::TranslatedMessage;
77use text_components::{
78    Modifier as _, TextComponent,
79    interactivity::{ClickEvent, HoverEvent},
80};
81use text_components::{content::Resolvable, custom::CustomData};
82
83use crate::behavior::InteractionResult;
84use crate::chunk::chunk_request::{ChunkRequestHandle, ChunkRequestState};
85use crate::config::RuntimeConfig;
86use crate::enchantment_helper;
87use crate::entity::damage::DamageSource;
88use crate::entity::{
89    DEATH_DURATION, Entity, EntityAnchor, EntityBase, EntityEventSource, EntityMovementEmission,
90    EntitySyncedData, LivingEntity, LivingEntityBase, MobEffectSyncChange, MobEffectSyncPacket,
91    RemovalReason, SharedEntity, apply_entity_look_at, start_riding_entities,
92};
93use crate::fluid::get_fluid_state;
94use crate::inventory::equipment::{EntityEquipment, EquipmentSlot};
95use crate::inventory::lock::{ContainerLockGuard, ContainerRef};
96use crate::inventory::menu::Menu;
97use crate::inventory::menu::kinds::inventory_menu;
98use crate::level_data::RespawnData;
99use crate::permission::{
100    PermissionContext, PermissionExpr, PermissionMetadataSet, PermissionMetadataValue,
101    PermissionSet, PermissionState,
102};
103use crate::physics::MoveResult;
104use crate::player::experience::Experience;
105use crate::player::player_data::{PersistentEnderPearl, PersistentRootVehicle};
106use crate::player::player_inventory::{
107    MenuItemDisposition, MenuRemovalStatus, PlayerInventory, PlayerInventorySyncState,
108};
109use crate::server::{
110    Server,
111    jobs::{JobPoll, ServerJob, ServerJobContext},
112};
113use crate::world::player_spawn_finder::{PlayerSpawnSearch, PlayerSpawnSearchPoll};
114use steel_registry::vanilla_damage_types;
115
116use steel_protocol::packets::{
117    common::SCustomPayload,
118    game::{CContainerClose, CGameEvent, CSystemChat, GameEventType},
119};
120use steel_registry::RegistryEntry;
121use steel_registry::item_stack::ItemStack;
122
123use steel_utils::{
124    BlockPos, BlockStateId, ChunkPos, DowncastType, DowncastTypeKey, Identifier, UuidExt as _,
125};
126
127use crate::inventory::container::Container;
128
129const RESPAWN_SEARCH_READY_CANDIDATE_BUDGET: usize = 8;
130
131use crate::chunk::player_chunk_view::PlayerChunkView;
132use crate::player::chunk_sender::ChunkSender;
133use crate::portal::{
134    PortalTicketTarget, TeleportPostAction, TeleportPostTransition, TeleportTransition,
135};
136use crate::world::World;
137
138/// A struct representing a player.
139pub struct Player {
140    /// The player's game profile.
141    pub gameprofile: GameProfile,
142    /// The player's connection (abstracted for testing).
143    pub connection: Arc<PlayerConnection>,
144
145    /// The world the player is in.
146    pub world: ArcSwap<World>,
147
148    /// Reference to the server (for entity ID generation, etc.).
149    pub(crate) server: Weak<Server>,
150    /// Runtime configuration shared with the server.
151    pub(crate) config: Arc<RuntimeConfig>,
152
153    /// Common entity fields (id, uuid, position, rotation, removal, callback).
154    base: EntityBase,
155
156    /// Client lifecycle flags.
157    lifecycle: SyncMutex<PlayerLifecycleState>,
158
159    /// Movement tracking state
160    pub(crate) movement: SyncMutex<MovementState>,
161
162    /// Synchronized entity data (health, pose, flags, etc.) for network sync.
163    entity_data: SyncMutex<PlayerEntityData>,
164
165    /// The last chunk position of the player.
166    pub last_chunk_pos: SyncMutex<ChunkPos>,
167    /// The last chunk tracking view of the player.
168    pub last_tracking_view: SyncMutex<Option<PlayerChunkView>>,
169    /// The chunk sender for the player.
170    pub chunk_sender: SyncMutex<ChunkSender>,
171
172    /// The client's settings/information (language, view distance, chat visibility, etc.).
173    /// Updated when the client sends `SClientInformation` during config or play phase.
174    client_information: SyncMutex<ClientInformation>,
175
176    /// Chat state: message counters, signature cache, validator, session, chain.
177    pub chat: SyncMutex<ChatState>,
178
179    /// Current and previous game mode.
180    game_modes: SyncMutex<PlayerGameModeState>,
181
182    /// The player's inventory container (shared with `inventory_menu`).
183    pub inventory: Shared<PlayerInventory>,
184
185    /// Logical inventory slots that must be resent directly to this player's client.
186    inventory_sync: SyncMutex<PlayerInventorySyncState>,
187
188    /// Last main-hand stack used for vanilla attack-strength reset checks.
189    last_item_in_main_hand: SyncMutex<ItemStack>,
190
191    /// The player's inventory menu (always open, even when `container_id` is 0).
192    inventory_menu: SyncMutex<Menu>,
193
194    /// The currently open menu (None if player inventory is open).
195    /// This is separate from `inventory_menu` which is always present.
196    open_menu: SyncMutex<player_inventory::OpenMenuState>,
197
198    /// Counter for generating container IDs (1-100, wraps around).
199    container_counter: SyncMutex<ContainerCounter>,
200
201    /// Pending server-initiated teleport state (ID, position, timeout).
202    teleport_state: SyncMutex<TeleportState>,
203    /// Vanilla item use cooldown groups.
204    item_cooldowns: SyncMutex<ItemCooldowns>,
205
206    /// Local tick and once-per-tick packet state.
207    tick_state: SyncMutex<PlayerTickState>,
208
209    /// Player abilities (flight, invulnerability, build permissions, speeds, etc.)
210    pub abilities: SyncMutex<Abilities>,
211
212    /// Block breaking state machine.
213    pub block_breaking: SyncMutex<BlockBreakingManager>,
214
215    /// Shared living-entity runtime fields (attributes, speed, damage/death state).
216    /// Vanilla: `LivingEntity` (L230-232) + `Entity.invulnerableTime` (L256).
217    living_base: LivingEntityBase,
218
219    /// Player food/hunger state (food level, saturation, exhaustion).
220    pub food_data: SyncMutex<FoodData>,
221
222    /// Delta-tracking state for `CSetHealth` deduplication.
223    health_sync: SyncMutex<HealthSyncState>,
224
225    /// The Player's Experience
226    pub experience: SyncMutex<Experience>,
227
228    /// Assigned groups, direct overrides, and the effective permission set.
229    permissions: SyncMutex<PlayerPermissionState>,
230
231    /// Whether the player has completed the vanilla End credits flow.
232    seen_credits: SyncMutex<bool>,
233
234    /// Vanilla `ServerPlayer.wonGame`; transient while the End credits screen is open.
235    won_game: SyncMutex<bool>,
236
237    /// Monotonic counter bumped on world teleport/reset. The chunk sending tick
238    /// snapshots this before encoding and compares after to detect stale batches.
239    pub chunk_send_epoch: SyncMutex<u32>,
240
241    /// Domain-residence identity and persisted entities awaiting restoration.
242    residence: SyncMutex<PlayerResidenceState>,
243    /// In-flight ender pearls thrown by this player, kept weakly so they persist
244    /// with the player and re-spawn on login (vanilla `ServerPlayer.enderPearls`).
245    ender_pearls: SyncMutex<Vec<Weak<dyn Entity>>>,
246}
247
248// SAFETY: This key is owned by Steel and uniquely identifies `Player`.
249unsafe impl DowncastType for Player {
250    const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:entity/player");
251}
252
253#[derive(Clone)]
254struct PendingRootVehicleRestore {
255    world: Identifier,
256    root_vehicle: PersistentRootVehicle,
257}
258
259/// Runtime identity for one continuous stay in a Steel domain.
260#[derive(Clone, Copy, Debug, Eq, PartialEq)]
261pub(crate) struct DomainResidenceToken(u64);
262
263struct PlayerResidenceState {
264    token: DomainResidenceToken,
265    pending_root_vehicle: Option<PendingRootVehicleRestore>,
266    pending_ender_pearls: Vec<PersistentEnderPearl>,
267}
268
269impl PlayerResidenceState {
270    const fn new() -> Self {
271        Self {
272            token: DomainResidenceToken(1),
273            pending_root_vehicle: None,
274            pending_ender_pearls: Vec::new(),
275        }
276    }
277
278    fn advance(&mut self) -> DomainResidenceToken {
279        let Some(next_token) = self.token.0.checked_add(1) else {
280            panic!("domain residence token space exhausted");
281        };
282        self.token = DomainResidenceToken(next_token);
283        self.pending_root_vehicle = None;
284        self.pending_ender_pearls.clear();
285        self.token
286    }
287}
288
289impl Player {
290    /// Computes the start (eye position) and end positions for a raytrace.
291    pub fn get_ray_endpoints(&self) -> (DVec3, DVec3) {
292        let pos = self.position();
293        let start_pos = DVec3::new(pos.x, self.get_eye_y(), pos.z);
294        let block_interaction_range = self
295            .attributes()
296            .lock()
297            .get_value(vanilla_attributes::BLOCK_INTERACTION_RANGE)
298            .unwrap_or(4.5);
299        let direction = self.look_angle() * block_interaction_range;
300
301        let end_pos = start_pos + direction;
302        (start_pos, end_pos)
303    }
304
305    /// Returns the player's current game mode.
306    #[must_use]
307    pub fn game_mode(&self) -> GameType {
308        self.game_modes.lock().current()
309    }
310
311    /// Returns the player's previous game mode.
312    #[must_use]
313    pub fn previous_game_mode(&self) -> Option<GameType> {
314        self.game_modes.lock().previous()
315    }
316
317    /// Restores current and previous game mode from persistent player data.
318    pub(crate) fn restore_game_modes(&self, current: GameType, previous: Option<GameType>) {
319        self.game_modes.lock().set_pair(current, previous);
320    }
321
322    /// Changes the current game mode and records the old current mode as previous.
323    fn change_game_mode_state(&self, game_mode: GameType) -> bool {
324        self.game_modes.lock().change_current(game_mode)
325    }
326
327    /// Creates a new player.
328    pub fn new(
329        gameprofile: GameProfile,
330        connection: Arc<PlayerConnection>,
331        world: Arc<World>,
332        server: Weak<Server>,
333        config: Arc<RuntimeConfig>,
334        entity_id: i32,
335        client_information: ClientInformation,
336    ) -> Self {
337        // Create a single shared inventory container used by both the player and inventory menu
338        let inventory = Arc::new(SyncMutex::new(PlayerInventory::new()));
339
340        let pos = DVec3::new(0.0, 0.0, 0.0);
341
342        let equipment = inventory.clone();
343        let living_base = LivingEntityBase::with_equipment(&vanilla_entities::PLAYER, equipment);
344        let player_uuid = gameprofile.id;
345        let world_ref = Arc::downgrade(&world);
346        let chat_spam_threshold_seconds = config.chat_spam_threshold_seconds;
347        let command_spam_threshold_seconds = config.command_spam_threshold_seconds;
348
349        Self {
350            gameprofile,
351            connection,
352
353            world: ArcSwap::new(world),
354            server,
355            config,
356            base: EntityBase::with_uuid(
357                entity_id,
358                player_uuid,
359                pos,
360                Self::dimensions_for_pose(EntityPose::Standing),
361                world_ref,
362            ),
363            lifecycle: SyncMutex::new(PlayerLifecycleState::default()),
364            movement: SyncMutex::new(MovementState::new()),
365            entity_data: SyncMutex::new({
366                let mut data = PlayerEntityData::new();
367                living_base.initialize_synced_data(&mut data);
368                data
369            }),
370            last_chunk_pos: SyncMutex::new(ChunkPos::new(0, 0)),
371            last_tracking_view: SyncMutex::new(None),
372            chunk_sender: SyncMutex::new(ChunkSender::default()),
373            client_information: SyncMutex::new(client_information),
374            chat: SyncMutex::new(ChatState::new(
375                chat_spam_threshold_seconds,
376                command_spam_threshold_seconds,
377            )),
378            game_modes: SyncMutex::new(PlayerGameModeState::new(GameType::Survival)),
379            inventory: inventory.clone(),
380            inventory_sync: SyncMutex::new(PlayerInventorySyncState::new()),
381            last_item_in_main_hand: SyncMutex::new(ItemStack::empty()),
382            inventory_menu: SyncMutex::new(inventory_menu(inventory)),
383            open_menu: SyncMutex::new(player_inventory::OpenMenuState::new()),
384            container_counter: SyncMutex::new(ContainerCounter::new()),
385            teleport_state: SyncMutex::new(TeleportState::new()),
386            item_cooldowns: SyncMutex::new(ItemCooldowns::default()),
387            tick_state: SyncMutex::new(PlayerTickState::new()),
388            abilities: SyncMutex::new(Abilities::default()),
389            block_breaking: SyncMutex::new(BlockBreakingManager::new()),
390            living_base,
391            food_data: SyncMutex::new(FoodData::new()),
392            health_sync: SyncMutex::new(HealthSyncState::new()),
393            experience: SyncMutex::new(Experience::default()),
394            permissions: SyncMutex::new(PlayerPermissionState::default()),
395            seen_credits: SyncMutex::new(false),
396            won_game: SyncMutex::new(false),
397            chunk_send_epoch: SyncMutex::new(0),
398            residence: SyncMutex::new(PlayerResidenceState::new()),
399            ender_pearls: SyncMutex::new(Vec::new()),
400        }
401    }
402
403    /// Ticks the player.
404    ///
405    /// # Panics
406    ///
407    /// Panics if the player position cannot be restored after `ai_step`. Vanilla treats the
408    /// pre-tick position as authoritative here, so a rejection indicates corrupted entity state.
409    pub fn tick(&self) {
410        self.advance_tick();
411        self.tick_item_cooldowns();
412        self.tick_attack_strength();
413        self.tick_spam_throttlers();
414        self.tick_client_load_timeout();
415
416        self.set_no_physics(self.is_spectator());
417        if self.is_spectator() || self.is_passenger() {
418            self.set_on_ground(false);
419        }
420
421        let tick_position = self.position();
422
423        // Vanilla: ServerGamePacketListenerImpl.resetPosition().
424        self.movement.lock().reset_for_tick(tick_position);
425        self.set_old_position_to_current();
426        self.reset_vehicle_movement_for_tick();
427
428        self.default_tick();
429        self.detect_equipment_updates();
430        self.ai_step();
431
432        // Vanilla snaps the player back to firstGood after ServerPlayer.doTick().
433        if let Err(error) = self.try_set_position(tick_position) {
434            panic!(
435                "failed to restore player {} tick position after ai_step: {error}",
436                self.id()
437            );
438        }
439        self.refresh_fluid_contact();
440
441        self.tick_ack_block_changes();
442
443        if !self.has_client_loaded() {
444            //return;
445        }
446
447        self.living_base.decrement_invulnerable_time();
448        self.tick_mob_effects();
449
450        if self.get_health() <= 0.0 {
451            self.tick_death();
452        } else {
453            let world = self.get_world();
454            self.touch_nearby_items();
455            self.block_breaking.lock().tick(self, &world);
456
457            // TODO: Implement remaining player ticking logic here
458            // - Managing game mode specific logic
459            // - Updating advancements
460            // - Handling falling
461
462            self.update_player_attributes();
463            self.living_base.refresh_speed_from_attributes();
464            self.tick_regeneration();
465
466            if self.is_sprinting() && !self.food_data.lock().has_enough_food() {
467                self.set_sprinting(false);
468            }
469        }
470
471        if self.disconnect_if_floating_too_long() {
472            return;
473        }
474        if self.disconnect_if_vehicle_floating_too_long() {
475            return;
476        }
477
478        self.tick_living_state();
479
480        self.tick_open_menu();
481        self.flush_inventory_resync();
482        self.broadcast_inventory_changes();
483        self.update_pose();
484
485        {
486            let health = self.get_health();
487            let (food, saturation) = {
488                let food_data = self.food_data.lock();
489                (food_data.food_level, food_data.saturation_level)
490            };
491
492            let saturation_zero = saturation == 0.0;
493
494            let mut sync = self.health_sync.lock();
495            if sync.needs_update(health, food, saturation_zero) {
496                self.send_packet(CSetHealth {
497                    health,
498                    food,
499                    food_saturation: saturation,
500                });
501                sync.record_sent(health, food, saturation_zero);
502            }
503        }
504
505        let experience_packet = {
506            let mut experience = self.experience.lock();
507            if experience.dirty {
508                experience.dirty = false;
509                Some(CSetExperience {
510                    progress: experience.progress(),
511                    level: experience.level(),
512                    total_experience: experience.total_points(),
513                })
514            } else {
515                None
516            }
517        };
518        if let Some(packet) = experience_packet {
519            self.send_packet(packet);
520        }
521
522        self.connection.tick();
523    }
524
525    /// Ticks the death animation timer.
526    /// Vanilla: `LivingEntity.tickDeath()` (not overridden by `ServerPlayer`).
527    fn tick_death(&self) {
528        let death_time = self.living_base.increment_death_time();
529
530        if death_time >= DEATH_DURATION && !self.is_removed() {
531            let world = self.get_world();
532            let chunk_pos = *self.last_chunk_pos.lock();
533            world.broadcast_to_nearby(
534                chunk_pos,
535                CEntityEvent {
536                    entity_id: self.id(),
537                    event: EntityStatus::Poof,
538                },
539                None,
540            );
541
542            world.unregister_player_entity(self);
543            world.chunk_map.remove_player(self);
544            world.entity_tracker().on_player_leave(self.id());
545            world.player_area_map.remove_by_entity_id(self.id());
546            self.set_removed(RemovalReason::Killed);
547            assert_eq!(
548                self.remove_all_menus_with_disposition(MenuItemDisposition::Drop),
549                MenuRemovalStatus::Complete,
550                "death removal menu cleanup must run outside a menu callback"
551            );
552        }
553    }
554
555    /// Immediately flushes dirty player entity data to tracking players and self.
556    fn sync_entity_data(&self) {
557        if let Some(dirty_values) = self.entity_data.lock().pack_dirty() {
558            let packet = CSetEntityData::new(self.id(), dirty_values);
559            self.get_world()
560                .broadcast_to_entity_trackers(self.id(), packet.clone(), None);
561            self.send_packet(packet);
562        }
563    }
564
565    fn update_dirty_mob_effect_entity_data(&self) {
566        if !self.living_base.take_effects_dirty() {
567            return;
568        }
569
570        let mut display = self.living_base.mob_effect_display_state();
571        if self.game_mode() == GameType::Spectator {
572            display.particles = ParticleList::default();
573            display.invisible = true;
574        }
575
576        {
577            let mut entity_data = self.entity_data.lock();
578            let living = entity_data.living_entity_mut();
579            living.effect_particles.set(display.particles);
580            living.effect_ambience.set(display.ambient);
581        }
582
583        self.entity_data.set_base_invisible_flag(display.invisible);
584        self.entity_data
585            .set_base_glowing_flag(self.has_glowing_tag() || display.glowing);
586    }
587
588    /// Handles a custom payload packet.
589    #[expect(clippy::unused_self, reason = "this is an api function")]
590    pub fn handle_custom_payload(&self, _packet: SCustomPayload) {}
591
592    /// Handles the end of a client tick.
593    pub fn handle_client_tick_end(&self) {
594        self.movement.lock().finish_client_tick();
595    }
596
597    /// Main entry point for dealing damage. Returns `true` if damage was applied.
598    ///
599    /// `world` is vanilla's explicit `ServerLevel` argument and controls
600    /// difficulty scaling and damage gamerules.
601    pub fn hurt(&self, world: &World, source: &DamageSource, amount: f32) -> bool {
602        if LivingEntity::is_invulnerable_to(self, world, source) {
603            return false;
604        }
605
606        {
607            let abilities = self.abilities.lock();
608            if abilities.invulnerable && !source.bypasses_invulnerability() {
609                return false;
610            }
611        }
612
613        // TODO: reset player noActionTime and remove shoulder entities.
614        if self.get_health() <= 0.0 {
615            return false;
616        }
617
618        // Difficulty scaling (vanilla: Player.hurtServer)
619        let mut amount = amount;
620        let causing_entity = source
621            .causing_entity_id
622            .and_then(|entity_id| world.get_entity_by_id(entity_id));
623        if source.scales_with_difficulty(causing_entity.as_deref()) {
624            let difficulty = world.level_data.read().data().difficulty;
625            match difficulty {
626                Difficulty::Peaceful => {
627                    amount = 0.0;
628                }
629                Difficulty::Easy => {
630                    amount = (amount / 2.0 + 1.0).min(amount);
631                }
632                Difficulty::Hard => {
633                    amount = amount * 3.0 / 2.0;
634                }
635                Difficulty::Normal => {}
636            }
637        }
638
639        if amount == 0.0 {
640            return false;
641        }
642
643        LivingEntity::hurt_server(self, world, source, amount)
644    }
645
646    fn disabled_damage_game_rule(source: &DamageSource) -> Option<GameRuleRef<bool>> {
647        if source.is(&vanilla_damage_type_tags::DamageTypeTag::IS_DROWNING) {
648            Some(&DROWNING_DAMAGE)
649        } else if source.is(&vanilla_damage_type_tags::DamageTypeTag::IS_FALL) {
650            Some(&FALL_DAMAGE)
651        } else if source.is(&vanilla_damage_type_tags::DamageTypeTag::IS_FIRE) {
652            Some(&FIRE_DAMAGE)
653        } else if source.is(&vanilla_damage_type_tags::DamageTypeTag::IS_FREEZING) {
654            Some(&FREEZE_DAMAGE)
655        } else {
656            None
657        }
658    }
659
660    /// Applies vanilla player damage reductions and health loss.
661    fn actually_hurt(&self, world: &World, source: &DamageSource, amount: f32) {
662        if LivingEntity::is_invulnerable_to(self, world, source) {
663            return;
664        }
665
666        let damage = LivingEntity::get_damage_after_armor_absorb(self, source, amount);
667        let damage = LivingEntity::get_damage_after_magic_absorb(self, source, damage);
668        let original_damage = damage;
669        let damage = (damage - self.get_absorption_amount()).max(0.0);
670        self.set_absorption_amount(self.get_absorption_amount() - (original_damage - damage));
671
672        // TODO: combat tracker (getCombatTracker().recordDamage)
673        if damage != 0.0 {
674            self.cause_food_exhaustion(source.damage_type.exhaustion);
675            self.set_health(self.get_health() - damage);
676            self.game_event(&vanilla_game_events::ENTITY_DAMAGE);
677        }
678    }
679
680    /// Vanilla: `ServerPlayer.die()` (does NOT call `super.die()`).
681    fn die(&self, source: &DamageSource) {
682        if self.is_removed() {
683            return;
684        }
685        if !self.living_base.mark_death_processed() {
686            return;
687        }
688
689        self.game_event(&vanilla_game_events::ENTITY_DIE);
690
691        self.sync_entity_data();
692
693        // NOTE: Vanilla `ServerPlayer.die()` does NOT set Pose::Dying — only
694        // `LivingEntity.die()` does (which ServerPlayer never calls via super).
695        // The death screen covers the player model, so the pose is irrelevant.
696
697        let world = self.get_world();
698
699        // Broadcast entity event 3 (death sound) to all nearby players.
700        let chunk_pos = *self.last_chunk_pos.lock();
701        world.broadcast_to_nearby(
702            chunk_pos,
703            CEntityEvent {
704                entity_id: self.id(),
705                event: EntityStatus::Death,
706            },
707            None,
708        );
709
710        let show_death_messages = world.get_game_rule(&SHOW_DEATH_MESSAGES);
711
712        // TODO: use CombatTracker for multi-arg messages (killer name, item, etc.)
713        let death_key = format!("death.attack.{}", source.damage_type.message_id);
714        let death_message = TranslatedMessage {
715            key: death_key.into(),
716            fallback: None,
717            args: Some(Box::new([TextComponent::plain(
718                self.gameprofile.name.clone(),
719            )])),
720        }
721        .component();
722
723        self.send_packet(CPlayerCombatKill {
724            player_id: self.id(),
725            message: if show_death_messages {
726                death_message.clone()
727            } else {
728                TextComponent::const_plain("")
729            },
730        });
731
732        // TODO: team death message visibility (ALWAYS / HIDE_FOR_OTHER_TEAMS / HIDE_FOR_OWN_TEAM)
733        if show_death_messages {
734            world.broadcast_system_chat(CSystemChat {
735                content: death_message,
736                overlay: false,
737            });
738        }
739
740        if !world.get_game_rule(&KEEP_INVENTORY) {
741            let items: Vec<ItemStack> = {
742                let mut inventory = self.inventory.lock();
743                (0..inventory.get_container_size())
744                    .filter_map(|slot| {
745                        let item = inventory.get_item(slot).clone();
746                        if item.is_empty() {
747                            None
748                        } else {
749                            inventory.set_item(slot, ItemStack::empty());
750                            Some(item)
751                        }
752                    })
753                    .collect()
754            };
755            for item in items {
756                let _ = self.drop_item(item, true, false);
757            }
758        }
759
760        self.clear_fire();
761        self.set_ticks_frozen(0);
762
763        if world.get_game_rule(&IMMEDIATE_RESPAWN) {
764            self.respawn();
765        }
766    }
767
768    /// Returns whether the Player can eat
769    pub fn can_eat(&self, can_always_eat: bool) -> bool {
770        let invulnerable = { self.abilities.lock().invulnerable };
771        let needs_foods = { self.food_data.lock().needs_food() };
772        invulnerable || can_always_eat || needs_foods
773    }
774
775    /// Cleans up player resources.
776    #[expect(clippy::unused_self, reason = "this is an api function")]
777    pub const fn cleanup(&self) {}
778
779    /// Returns the world the player is currently in.
780    pub fn get_world(&self) -> Arc<World> {
781        self.world.load_full()
782    }
783
784    /// Returns the server this player belongs to.
785    pub(crate) fn server(&self) -> Arc<Server> {
786        self.server
787            .upgrade()
788            .expect("player must not outlive server")
789    }
790
791    /// Returns the identity of the player's current continuous domain stay.
792    pub(crate) fn domain_residence_token(&self) -> DomainResidenceToken {
793        self.residence.lock().token
794    }
795
796    /// Starts a new continuous domain stay and invalidates old restore work.
797    pub(crate) fn advance_domain_residence(&self) -> DomainResidenceToken {
798        self.residence.lock().advance()
799    }
800
801    /// Returns whether delayed work still belongs to the current domain stay.
802    pub(crate) fn is_domain_residence_current(&self, token: DomainResidenceToken) -> bool {
803        self.residence.lock().token == token
804    }
805
806    /// Installs both persisted restore payloads for a token-owned domain stay.
807    pub(crate) fn install_pending_domain_restores(
808        &self,
809        token: DomainResidenceToken,
810        world: &World,
811        root_vehicle: Option<PersistentRootVehicle>,
812        ender_pearls: Vec<PersistentEnderPearl>,
813    ) -> bool {
814        let mut residence = self.residence.lock();
815        if residence.token != token {
816            return false;
817        }
818
819        residence.pending_root_vehicle =
820            root_vehicle.map(|root_vehicle| PendingRootVehicleRestore {
821                world: world.key.clone(),
822                root_vehicle,
823            });
824        residence.pending_ender_pearls = ender_pearls;
825        true
826    }
827
828    pub(crate) fn clear_pending_root_vehicle(&self) {
829        self.residence.lock().pending_root_vehicle = None;
830    }
831
832    pub(crate) fn pending_root_vehicle_for_current_world(&self) -> Option<PersistentRootVehicle> {
833        let world_key = self.get_world().key.clone();
834        self.residence
835            .lock()
836            .pending_root_vehicle
837            .as_ref()
838            .filter(|pending| pending.world == world_key)
839            .map(|pending| pending.root_vehicle.clone())
840    }
841
842    pub(crate) fn take_matching_pending_root_vehicle(
843        &self,
844        token: DomainResidenceToken,
845        world: &World,
846        attach: [u8; 16],
847        root_uuid: [u8; 16],
848    ) -> Option<PersistentRootVehicle> {
849        let mut residence = self.residence.lock();
850        if residence.token != token {
851            return None;
852        }
853        let matches = residence
854            .pending_root_vehicle
855            .as_ref()
856            .is_some_and(|pending| {
857                pending.world == world.key
858                    && pending.root_vehicle.attach == attach
859                    && pending.root_vehicle.entity.uuid == root_uuid
860            });
861        if matches {
862            residence
863                .pending_root_vehicle
864                .take()
865                .map(|pending| pending.root_vehicle)
866        } else {
867            None
868        }
869    }
870
871    pub(crate) fn pending_ender_pearls(&self) -> Vec<PersistentEnderPearl> {
872        self.residence.lock().pending_ender_pearls.clone()
873    }
874
875    pub(crate) fn remove_pending_ender_pearl(&self, uuid: Uuid) {
876        self.residence
877            .lock()
878            .pending_ender_pearls
879            .retain(|pearl| Uuid::from_bytes(pearl.entity.uuid) != uuid);
880    }
881
882    pub(crate) fn discard_pending_ender_pearl(
883        &self,
884        token: DomainResidenceToken,
885        uuid: Uuid,
886    ) -> bool {
887        let mut residence = self.residence.lock();
888        if residence.token != token {
889            return false;
890        }
891        let old_len = residence.pending_ender_pearls.len();
892        residence
893            .pending_ender_pearls
894            .retain(|pearl| Uuid::from_bytes(pearl.entity.uuid) != uuid);
895        residence.pending_ender_pearls.len() != old_len
896    }
897
898    pub(crate) fn take_matching_pending_ender_pearl(
899        &self,
900        token: DomainResidenceToken,
901        world: &World,
902        uuid: Uuid,
903    ) -> Option<PersistentEnderPearl> {
904        let mut residence = self.residence.lock();
905        if residence.token != token {
906            return None;
907        }
908        let world_key = world.key.to_string();
909        let index = residence.pending_ender_pearls.iter().position(|pearl| {
910            pearl.world == world_key && Uuid::from_bytes(pearl.entity.uuid) == uuid
911        })?;
912        Some(residence.pending_ender_pearls.remove(index))
913    }
914
915    /// Registers a thrown ender pearl so it persists with this player and
916    /// re-spawns on login (vanilla `ServerPlayer.registerEnderPearl`).
917    pub fn register_ender_pearl(&self, pearl: &SharedEntity) {
918        let uuid = pearl.uuid();
919        let mut pearls = self.ender_pearls.lock();
920        pearls.retain(|weak| {
921            weak.upgrade()
922                .is_some_and(|p| !p.is_removed() && p.uuid() != uuid)
923        });
924        pearls.push(Arc::downgrade(pearl));
925        drop(pearls);
926        self.remove_pending_ender_pearl(uuid);
927    }
928
929    /// Deregisters a thrown ender pearl once it hits, teleports, or is discarded
930    /// (vanilla `ServerPlayer.deregisterEnderPearl`).
931    pub fn deregister_ender_pearl(&self, uuid: Uuid) {
932        self.ender_pearls
933            .lock()
934            .retain(|weak| weak.upgrade().is_some_and(|p| p.uuid() != uuid));
935    }
936
937    /// Returns this player's live, in-flight ender pearls, pruning dead entries.
938    #[must_use]
939    pub fn ender_pearls(&self) -> Vec<SharedEntity> {
940        let mut pearls = self.ender_pearls.lock();
941        pearls.retain(|weak| weak.upgrade().is_some_and(|p| !p.is_removed()));
942        pearls.iter().filter_map(Weak::upgrade).collect()
943    }
944
945    /// Appends vanilla-shaped player state used by command NBT predicates.
946    pub(crate) fn save_command_nbt(&self, nbt: &mut NbtCompound) {
947        {
948            let inventory = self.inventory.lock();
949            nbt.insert("Inventory", inventory.to_vanilla_inventory_nbt());
950            nbt.insert("SelectedItemSlot", i32::from(inventory.get_selected_slot()));
951        }
952
953        {
954            let experience = self.experience.lock();
955            nbt.insert("XpP", experience.progress());
956            nbt.insert("XpLevel", experience.level());
957            nbt.insert("XpTotal", experience.total_points());
958        }
959        nbt.insert("Score", self.score());
960
961        {
962            let food = self.food_data.lock();
963            nbt.insert("foodLevel", food.food_level);
964            nbt.insert("foodTickTimer", food.tick_timer);
965            nbt.insert("foodSaturationLevel", food.saturation_level);
966            nbt.insert("foodExhaustionLevel", food.exhaustion_level);
967        }
968
969        {
970            let abilities = self.abilities.lock();
971            let mut abilities_nbt = NbtCompound::new();
972            abilities_nbt.insert(
973                "invulnerable",
974                NbtTag::Byte(i8::from(abilities.invulnerable)),
975            );
976            abilities_nbt.insert("flying", NbtTag::Byte(i8::from(abilities.flying)));
977            abilities_nbt.insert("mayfly", NbtTag::Byte(i8::from(abilities.may_fly)));
978            abilities_nbt.insert("instabuild", NbtTag::Byte(i8::from(abilities.instabuild)));
979            abilities_nbt.insert("mayBuild", NbtTag::Byte(i8::from(abilities.may_build)));
980            abilities_nbt.insert("flySpeed", abilities.flying_speed);
981            abilities_nbt.insert("walkSpeed", abilities.walking_speed);
982            nbt.insert("abilities", NbtTag::Compound(abilities_nbt));
983        }
984
985        nbt.insert("playerGameType", self.game_mode() as i32);
986        if let Some(previous_game_mode) = self.previous_game_mode() {
987            nbt.insert("previousPlayerGameType", previous_game_mode as i32);
988        }
989        nbt.insert(
990            "seenCredits",
991            NbtTag::Byte(i8::from(self.has_seen_credits())),
992        );
993        nbt.insert("Dimension", self.get_world().key.to_string());
994
995        if let Some(vehicle) = self.vehicle()
996            && let Some(root_vehicle) = self.root_vehicle()
997            && root_vehicle.id() != self.id()
998            && root_vehicle.has_exactly_one_player_passenger()
999            && let Some(entity_nbt) = root_vehicle.nbt_for_passenger_save()
1000        {
1001            let mut root_vehicle_nbt = NbtCompound::new();
1002            root_vehicle_nbt.insert(
1003                "Attach",
1004                NbtTag::IntArray(vehicle.uuid().to_int_array().to_vec()),
1005            );
1006            root_vehicle_nbt.insert("Entity", NbtTag::Compound(entity_nbt));
1007            nbt.insert("RootVehicle", NbtTag::Compound(root_vehicle_nbt));
1008        }
1009
1010        let ender_pearls = self
1011            .ender_pearls()
1012            .into_iter()
1013            .filter_map(|pearl| {
1014                let world = pearl.level()?;
1015                let mut pearl_nbt = pearl.nbt_for_passenger_save()?;
1016                pearl_nbt.insert("ender_pearl_dimension", world.key.to_string());
1017                Some(pearl_nbt)
1018            })
1019            .collect::<Vec<_>>();
1020        if !ender_pearls.is_empty() {
1021            nbt.insert("ender_pearls", NbtList::Compound(ender_pearls));
1022        }
1023    }
1024
1025    /// Marks live ender pearls as stored with this player so chunk saves remove
1026    /// them from world storage and player data remains the sole owner.
1027    pub fn store_ender_pearls_with_player(&self) {
1028        for pearl in self.ender_pearls() {
1029            let world = pearl.level();
1030            let chunk = ChunkPos::from_entity_pos(pearl.position());
1031            pearl.set_removed(RemovalReason::StoredWithPlayer);
1032            if let Some(world) = world {
1033                world.mark_chunk_dirty(chunk);
1034            }
1035        }
1036    }
1037
1038    /// Returns this player's local server tick count.
1039    #[must_use]
1040    pub fn tick_count(&self) -> i32 {
1041        self.tick_state.lock().tick_count()
1042    }
1043
1044    /// Returns vanilla `Player.takeXpDelay`.
1045    #[must_use]
1046    pub(crate) fn take_xp_delay(&self) -> i32 {
1047        self.tick_state.lock().take_xp_delay()
1048    }
1049
1050    /// Sets vanilla `Player.takeXpDelay`.
1051    pub(crate) fn set_take_xp_delay(&self, delay: i32) {
1052        self.tick_state.lock().set_take_xp_delay(delay);
1053    }
1054
1055    fn primary_step_sound_block_pos(&self, affecting_pos: BlockPos) -> BlockPos {
1056        let above_pos = affecting_pos.above();
1057        let above_state = self.get_world().get_block_state(above_pos);
1058        let above_block = above_state.get_block();
1059
1060        if above_block.has_tag(&BlockTag::INSIDE_STEP_SOUND_BLOCKS)
1061            || above_block.has_tag(&BlockTag::COMBINATION_STEP_SOUND_BLOCKS)
1062        {
1063            above_pos
1064        } else {
1065            affecting_pos
1066        }
1067    }
1068}
1069
1070impl Entity for Player {
1071    fn base(&self) -> &EntityBase {
1072        &self.base
1073    }
1074
1075    fn entity_type(&self) -> EntityTypeRef {
1076        &vanilla_entities::PLAYER
1077    }
1078
1079    fn base_tick(&self) {
1080        LivingEntity::base_tick_living_entity(self);
1081    }
1082
1083    fn scoreboard_name(&self) -> String {
1084        self.gameprofile.name.clone()
1085    }
1086
1087    fn name(&self) -> TextComponent {
1088        TextComponent::plain(self.gameprofile.name.clone())
1089    }
1090
1091    fn display_name(&self) -> TextComponent {
1092        self.name()
1093            .click_event(ClickEvent::suggest_command(format!(
1094                "/tell {} ",
1095                self.gameprofile.name
1096            )))
1097            .hover_event(HoverEvent::show_entity(
1098                "minecraft:player",
1099                self.uuid(),
1100                Some(self.name()),
1101            ))
1102            .insertion(self.gameprofile.name.clone())
1103    }
1104
1105    fn plain_text_name(&self) -> String {
1106        self.gameprofile.name.clone()
1107    }
1108
1109    fn look_at(&self, from_anchor: EntityAnchor, target: DVec3) {
1110        apply_entity_look_at(self, from_anchor, target);
1111        self.send_packet(CPlayerLookAt::position(
1112            protocol_look_at_anchor(from_anchor),
1113            target,
1114        ));
1115    }
1116
1117    fn look_at_entity(
1118        &self,
1119        from_anchor: EntityAnchor,
1120        target: &dyn Entity,
1121        target_anchor: EntityAnchor,
1122    ) {
1123        let target_position = target_anchor.position(target);
1124        apply_entity_look_at(self, from_anchor, target_position);
1125        self.send_packet(CPlayerLookAt::entity(
1126            protocol_look_at_anchor(from_anchor),
1127            target_position,
1128            target.id(),
1129            protocol_look_at_anchor(target_anchor),
1130        ));
1131    }
1132
1133    fn is_always_ticking(&self) -> bool {
1134        true
1135    }
1136
1137    fn update_swimming(&self) {
1138        if self.is_flying() {
1139            self.set_shared_swimming(false);
1140        } else {
1141            self.default_update_swimming();
1142        }
1143    }
1144
1145    fn stop_riding(&self) {
1146        let old_vehicle = self.vehicle();
1147        self.base().stop_riding();
1148        let Some(old_vehicle) = old_vehicle else {
1149            return;
1150        };
1151
1152        self.remove_active_effects_for_vehicle(old_vehicle.as_ref());
1153        self.send_packet(CSetPassengers::new(
1154            old_vehicle.id(),
1155            Self::passenger_ids_for_packet(old_vehicle.as_ref()),
1156        ));
1157    }
1158
1159    fn start_riding(&self, entity_to_ride: &SharedEntity) -> bool {
1160        let Some(world) = self.level() else {
1161            return false;
1162        };
1163        let Some(passenger) = world.get_entity_by_id(self.id()) else {
1164            return false;
1165        };
1166        if !start_riding_entities(&passenger, entity_to_ride) {
1167            return false;
1168        }
1169
1170        entity_to_ride.position_rider(self.as_entity_event_source());
1171        let position = self.position();
1172        let (yaw, pitch) = self.rotation();
1173        if let Err(error) = self.teleport(position, yaw, pitch) {
1174            panic!(
1175                "failed to synchronize player {} mounted position: {error}",
1176                self.id()
1177            );
1178        }
1179        self.send_active_effects_for_vehicle(entity_to_ride.as_ref());
1180        self.send_packet(CSetPassengers::new(
1181            entity_to_ride.id(),
1182            Self::passenger_ids_for_packet(entity_to_ride.as_ref()),
1183        ));
1184        true
1185    }
1186
1187    fn broadcast_to_player(&self, player: &Player) -> bool {
1188        if player.is_spectator() {
1189            true
1190        } else {
1191            !self.is_spectator()
1192        }
1193    }
1194
1195    fn fall_sounds(&self) -> (SoundEventRef, SoundEventRef) {
1196        (
1197            &sound_events::ENTITY_PLAYER_SMALL_FALL,
1198            &sound_events::ENTITY_PLAYER_BIG_FALL,
1199        )
1200    }
1201
1202    fn is_alive(&self) -> bool {
1203        !self.is_removed() && self.get_health() > 0.0
1204    }
1205
1206    fn forces_fall_flying_velocity_sync(&self) -> bool {
1207        self.is_fall_flying()
1208    }
1209
1210    fn blocks_building(&self) -> bool {
1211        true
1212    }
1213
1214    fn is_pickable(&self) -> bool {
1215        !self.is_spectator() && !self.is_removed()
1216    }
1217
1218    fn is_pushable(&self) -> bool {
1219        self.get_health() > 0.0 && !self.is_spectator() && !self.on_climbable()
1220    }
1221
1222    fn on_climbable(&self) -> bool {
1223        Player::on_climbable(self)
1224    }
1225
1226    fn is_spectator(&self) -> bool {
1227        self.game_mode() == GameType::Spectator
1228    }
1229
1230    fn is_flying_player(&self) -> bool {
1231        self.is_flying()
1232    }
1233
1234    fn fire_immune_ticks(&self) -> i32 {
1235        20
1236    }
1237
1238    fn remaining_fire_ticks_cap(&self) -> Option<i32> {
1239        self.abilities.lock().invulnerable.then_some(1)
1240    }
1241
1242    fn get_default_gravity(&self) -> f64 {
1243        LivingEntity::get_attribute_gravity(self)
1244    }
1245
1246    fn fire_ignite_extra_ticks(&self) -> i32 {
1247        rand::random_range(1..=2)
1248    }
1249
1250    fn can_freeze(&self) -> bool {
1251        if self.is_spectator() {
1252            return false;
1253        }
1254
1255        self.default_living_can_freeze()
1256    }
1257
1258    fn make_stuck_in_block(&self, state: BlockStateId, speed_multiplier: DVec3) {
1259        if !self.is_flying() {
1260            self.default_make_stuck_in_block(state, speed_multiplier);
1261        }
1262
1263        // TODO: Reset current impulse context once vehicle/player impulse contexts exist.
1264    }
1265
1266    fn can_be_hit_by_projectile(&self) -> bool {
1267        self.get_health() > 0.0 && self.is_pickable()
1268    }
1269
1270    fn uses_client_movement_packets(&self) -> bool {
1271        true
1272    }
1273
1274    fn can_simulate_movement(&self) -> bool {
1275        true
1276    }
1277
1278    fn is_effective_ai(&self) -> bool {
1279        true
1280    }
1281
1282    fn known_movement(&self) -> DVec3 {
1283        if let Some(vehicle) = self.vehicle()
1284            && vehicle
1285                .controlling_passenger()
1286                .is_none_or(|controller| controller.id() != self.id())
1287        {
1288            return vehicle.known_movement();
1289        }
1290
1291        self.movement.lock().last_known_client_movement()
1292    }
1293
1294    fn known_speed(&self) -> DVec3 {
1295        if let Some(vehicle) = self.vehicle()
1296            && vehicle
1297                .controlling_passenger()
1298                .is_none_or(|controller| controller.id() != self.id())
1299        {
1300            return vehicle.known_speed();
1301        }
1302
1303        self.movement.lock().last_known_client_movement()
1304    }
1305
1306    fn is_suppressing_bounce(&self) -> bool {
1307        self.is_crouching()
1308    }
1309
1310    fn cause_fall_damage(
1311        &self,
1312        fall_distance: f64,
1313        damage_modifier: f32,
1314        source: &DamageSource,
1315    ) -> bool {
1316        if self.abilities.lock().may_fly {
1317            return false;
1318        }
1319
1320        // TODO: Award `Stats.FALL_ONE_CM` once player statistics are implemented.
1321        LivingEntity::cause_living_fall_damage(self, fall_distance, damage_modifier, source)
1322    }
1323
1324    fn synced_data(&self) -> Option<&dyn EntitySyncedData> {
1325        Some(&self.entity_data)
1326    }
1327
1328    fn update_data_before_sync(&self) {
1329        self.update_dirty_mob_effect_entity_data();
1330    }
1331
1332    fn max_up_step(&self) -> f32 {
1333        self.attributes()
1334            .lock()
1335            .get_value(vanilla_attributes::STEP_HEIGHT)
1336            .unwrap_or(0.6) as f32
1337    }
1338
1339    fn backs_off_from_edge(&self) -> bool {
1340        self.is_crouching() && !self.is_flying()
1341    }
1342
1343    fn is_pushed_by_fluid(&self) -> bool {
1344        !self.is_flying()
1345    }
1346
1347    fn is_crouching(&self) -> bool {
1348        Player::is_crouching(self)
1349    }
1350
1351    fn may_interact(&self, world: &World, pos: BlockPos) -> bool {
1352        world.may_interact(self, pos)
1353    }
1354
1355    fn is_swimming(&self) -> bool {
1356        Player::is_swimming(self)
1357    }
1358
1359    fn sound_source(&self) -> SoundSource {
1360        SoundSource::Players
1361    }
1362
1363    fn swim_sound(&self) -> SoundEventRef {
1364        &sound_events::ENTITY_PLAYER_SWIM
1365    }
1366
1367    fn play_step_sound(&self, on_pos: BlockPos, on_state: BlockStateId) {
1368        if self.is_in_water() {
1369            self.water_swim_sound();
1370            self.play_muffled_step_sound(on_state);
1371            return;
1372        }
1373
1374        let primary_step_sound_pos = self.primary_step_sound_block_pos(on_pos);
1375        if primary_step_sound_pos == on_pos {
1376            self.play_block_step_sound(on_state);
1377        } else {
1378            let primary_state = self.get_world().get_block_state(primary_step_sound_pos);
1379            if primary_state
1380                .get_block()
1381                .has_tag(&BlockTag::COMBINATION_STEP_SOUND_BLOCKS)
1382            {
1383                self.play_combination_step_sounds(primary_state, on_state);
1384            } else {
1385                self.play_block_step_sound(primary_state);
1386            }
1387        }
1388    }
1389
1390    fn movement_emission(&self) -> EntityMovementEmission {
1391        if self.is_flying() || self.on_ground() && self.is_discrete() {
1392            EntityMovementEmission::None
1393        } else {
1394            EntityMovementEmission::All
1395        }
1396    }
1397
1398    fn on_below_world(&self) {
1399        let world = self.get_world();
1400        self.hurt(
1401            &world,
1402            &DamageSource::environment(&vanilla_damage_types::OUT_OF_WORLD),
1403            4.0,
1404        );
1405    }
1406
1407    fn dimensions_for_pose(&self, pose: EntityPose) -> EntityDimensions {
1408        let dimensions = Player::dimensions_for_pose(pose);
1409        if pose == EntityPose::Sleeping || self.entity_type().fixed {
1410            dimensions
1411        } else {
1412            dimensions.scale(LivingEntity::get_scale(self))
1413        }
1414    }
1415
1416    fn hurt(&self, world: &World, source: &DamageSource, amount: f32) -> bool {
1417        // Delegates to Player's inherent hurt method which handles
1418        // player-specific prechecks before the shared living hurt path.
1419        Player::hurt(self, world, source, amount)
1420    }
1421}
1422
1423const fn protocol_look_at_anchor(anchor: EntityAnchor) -> LookAtAnchor {
1424    match anchor {
1425        EntityAnchor::Feet => LookAtAnchor::Feet,
1426        EntityAnchor::Eyes => LookAtAnchor::Eyes,
1427    }
1428}
1429
1430impl LivingEntity for Player {
1431    fn tick_living_entity(&self) {
1432        Player::tick(self);
1433    }
1434
1435    fn get_health(&self) -> f32 {
1436        *self.entity_data.lock().living_entity().health.get()
1437    }
1438
1439    fn set_health(&self, health: f32) {
1440        let max_health = self.get_max_health();
1441        let clamped = health.clamp(0.0, max_health);
1442        self.entity_data
1443            .lock()
1444            .living_entity_mut()
1445            .health
1446            .set(clamped);
1447    }
1448
1449    fn living_base(&self) -> &LivingEntityBase {
1450        &self.living_base
1451    }
1452
1453    fn can_be_seen_as_enemy(&self) -> bool {
1454        !self.abilities.lock().invulnerable
1455            && !self.is_invulnerable()
1456            && self.can_be_seen_by_anyone()
1457    }
1458
1459    fn is_invulnerable_to(&self, world: &World, source: &DamageSource) -> bool {
1460        if self.default_is_invulnerable_to(source)
1461            || enchantment_helper::is_immune_to_damage(world, self, source)
1462        {
1463            return true;
1464        }
1465
1466        if let Some(rule) = Self::disabled_damage_game_rule(source) {
1467            return !world.get_game_rule(rule);
1468        }
1469
1470        !self.has_client_loaded()
1471    }
1472
1473    fn hurt_armor(&self, source: &DamageSource, damage: f32) {
1474        self.do_hurt_equipment(
1475            source,
1476            damage,
1477            &[
1478                EquipmentSlot::Feet,
1479                EquipmentSlot::Legs,
1480                EquipmentSlot::Chest,
1481                EquipmentSlot::Head,
1482            ],
1483        );
1484    }
1485
1486    fn actually_hurt(&self, world: &World, source: &DamageSource, amount: f32) {
1487        Player::actually_hurt(self, world, source, amount);
1488    }
1489
1490    fn hurt_broadcast_chunk(&self) -> ChunkPos {
1491        *self.last_chunk_pos.lock()
1492    }
1493
1494    fn die(&self, source: &DamageSource) {
1495        Player::die(self, source);
1496    }
1497
1498    fn with_equipment_slot(&self, slot: EquipmentSlot, visitor: &mut dyn FnMut(&ItemStack)) {
1499        let inventory = self.inventory.lock();
1500        visitor(inventory.get_ref(slot));
1501    }
1502
1503    fn with_equipment_slot_mut(
1504        &self,
1505        slot: EquipmentSlot,
1506        visitor: &mut dyn FnMut(&mut ItemStack),
1507    ) {
1508        let mut inventory = self.inventory.lock();
1509        inventory.with_equipment_item_mut(slot, visitor);
1510    }
1511
1512    fn interact_living_entity_with_equippable(
1513        &self,
1514        player: &Player,
1515        hand: InteractionHand,
1516    ) -> InteractionResult {
1517        let item_stack = {
1518            let inventory = player.inventory.lock();
1519            let item_stack = inventory.get_item_in_hand(hand);
1520            item_stack.copy_with_count(item_stack.count())
1521        };
1522        let Some(equippable) = item_stack.get_equippable() else {
1523            return InteractionResult::Pass;
1524        };
1525        if !equippable.equip_on_interact {
1526            return InteractionResult::Pass;
1527        }
1528
1529        let slot = equippable.slot;
1530        let can_equip = |stack: &ItemStack| {
1531            stack.get_equippable().is_some_and(|equippable| {
1532                equippable.equip_on_interact
1533                    && equippable.slot == slot
1534                    && self.is_equippable_in_slot(stack, slot)
1535            })
1536        };
1537        if !can_equip(&item_stack) || !Entity::is_alive(self) {
1538            return InteractionResult::Pass;
1539        }
1540
1541        let source_ref = ContainerRef::from(player.inventory.clone());
1542        let target_ref = ContainerRef::from(self.inventory.clone());
1543        let source_id = source_ref.container_id();
1544        let target_id = target_ref.container_id();
1545        let mut guard = ContainerLockGuard::lock_all(&[source_ref, target_ref]);
1546        let source_slot = match hand {
1547            InteractionHand::MainHand => EquipmentSlot::MainHand,
1548            InteractionHand::OffHand => EquipmentSlot::OffHand,
1549        };
1550
1551        let equipped = if source_id == target_id {
1552            let Some(inventory) = guard.get_typed_mut::<PlayerInventory>(source_id) else {
1553                unreachable!("player inventory container retains its concrete type");
1554            };
1555            if !can_equip(inventory.get_item_in_hand(hand)) || !inventory.get_ref(slot).is_empty() {
1556                return InteractionResult::Pass;
1557            }
1558
1559            let equipped = inventory.get_mut(source_slot).split(1);
1560            if equipped.is_empty() {
1561                return InteractionResult::Pass;
1562            }
1563            let equipped_for_effects = equipped.copy_with_count(1);
1564            *inventory.get_mut(slot) = equipped;
1565            equipped_for_effects
1566        } else {
1567            let Some((source_inventory, target_inventory)) =
1568                guard.get_two_typed_mut::<PlayerInventory, PlayerInventory>(source_id, target_id)
1569            else {
1570                unreachable!("player inventory containers retain their concrete type");
1571            };
1572            if !can_equip(source_inventory.get_item_in_hand(hand))
1573                || !target_inventory.get_ref(slot).is_empty()
1574            {
1575                return InteractionResult::Pass;
1576            }
1577
1578            let equipped = source_inventory.get_mut(source_slot).split(1);
1579            if equipped.is_empty() {
1580                return InteractionResult::Pass;
1581            }
1582            let equipped_for_effects = equipped.copy_with_count(1);
1583            *target_inventory.get_mut(slot) = equipped;
1584            equipped_for_effects
1585        };
1586        drop(guard);
1587
1588        player.inventory.lock().set_changed();
1589        if source_id != target_id {
1590            self.inventory.lock().set_changed();
1591        }
1592
1593        if let Some(sound) = self.equip_sound(slot, &equipped) {
1594            self.play_sound(sound, 1.0, 1.0);
1595        }
1596        // TODO: Emit EQUIP game event once game-event dispatch is implemented.
1597        InteractionResult::Success
1598    }
1599
1600    fn has_infinite_materials(&self) -> bool {
1601        Player::has_infinite_materials(self)
1602    }
1603
1604    fn get_absorption_amount(&self) -> f32 {
1605        *self.entity_data.lock().player_absorption.get()
1606    }
1607
1608    fn set_absorption_amount(&self, amount: f32) {
1609        let max_absorption = self
1610            .living_base
1611            .attributes()
1612            .lock()
1613            .required_value(vanilla_attributes::MAX_ABSORPTION) as f32;
1614        self.entity_data
1615            .lock()
1616            .player_absorption
1617            .set(amount.clamp(0.0, max_absorption));
1618    }
1619
1620    fn is_affected_by_fluids(&self) -> bool {
1621        !self.is_flying()
1622    }
1623
1624    fn can_glide(&self) -> bool {
1625        !self.is_flying() && self.default_can_glide()
1626    }
1627
1628    fn is_immobile(&self) -> bool {
1629        self.default_is_immobile() || self.is_sleeping()
1630    }
1631
1632    fn jump_from_ground(&self) {
1633        self.default_jump_from_ground();
1634        // TODO: Award Stats.JUMP once player statistics exist.
1635        if self.is_sprinting() {
1636            self.cause_food_exhaustion(0.2);
1637        } else {
1638            self.cause_food_exhaustion(0.05);
1639        }
1640    }
1641
1642    fn ai_step(&self) -> Option<MoveResult> {
1643        if self.is_flying() && !self.is_passenger() {
1644            self.reset_fall_distance();
1645        }
1646
1647        let result = self.default_ai_step();
1648        self.set_y_head_rot(self.rotation().0);
1649        result
1650    }
1651
1652    fn travel(&self, input: DVec3) -> Option<MoveResult> {
1653        if self.is_passenger() {
1654            return self.default_travel(input);
1655        }
1656
1657        if self.is_swimming() {
1658            let look_angle_y = self.look_angle().y;
1659            let multiplier = if look_angle_y < -0.2 { 0.085 } else { 0.06 };
1660            let has_fluid_above = self.level().is_some_and(|world| {
1661                let position = self.position();
1662                let pos = BlockPos::containing(position.x, position.y + 0.9, position.z);
1663                !get_fluid_state(&world, pos).is_empty()
1664            });
1665            if look_angle_y <= 0.0 || self.is_jumping() || has_fluid_above {
1666                let velocity = self.velocity();
1667                self.set_velocity(
1668                    velocity + DVec3::new(0.0, (look_angle_y - velocity.y) * multiplier, 0.0),
1669                );
1670            }
1671        }
1672
1673        if self.is_flying() {
1674            let original_movement_y = self.velocity().y;
1675            let result = self.default_travel(input);
1676            let velocity = self.velocity();
1677            self.set_velocity(DVec3::new(
1678                velocity.x,
1679                original_movement_y * 0.6,
1680                velocity.z,
1681            ));
1682            result
1683        } else {
1684            self.default_travel(input)
1685        }
1686    }
1687
1688    fn get_flying_speed(&self) -> f32 {
1689        if self.is_flying() && !self.is_passenger() {
1690            let flying_speed = self.abilities.lock().flying_speed;
1691            if self.is_sprinting() {
1692                flying_speed * 2.0
1693            } else {
1694                flying_speed
1695            }
1696        } else if self.is_sprinting() {
1697            0.025_999_999
1698        } else {
1699            0.02
1700        }
1701    }
1702}
1703
1704impl TextResolutor for Player {
1705    fn resolve_content(&self, _resolvable: &Resolvable) -> TextComponent {
1706        TextComponent::new()
1707    }
1708
1709    fn resolve_custom(&self, _data: &CustomData) -> Option<TextComponent> {
1710        None
1711    }
1712
1713    fn translate(&self, _key: &str) -> Option<String> {
1714        None
1715    }
1716}
1717
1718#[cfg(test)]
1719mod tests;