1mod abilities;
3pub mod chat;
4pub mod chunk_sender;
5pub mod connection;
7mod container_counter;
8mod entity_state;
9pub mod experience;
11pub mod food_data;
12pub 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
138pub struct Player {
140 pub gameprofile: GameProfile,
142 pub connection: Arc<PlayerConnection>,
144
145 pub world: ArcSwap<World>,
147
148 pub(crate) server: Weak<Server>,
150 pub(crate) config: Arc<RuntimeConfig>,
152
153 base: EntityBase,
155
156 lifecycle: SyncMutex<PlayerLifecycleState>,
158
159 pub(crate) movement: SyncMutex<MovementState>,
161
162 entity_data: SyncMutex<PlayerEntityData>,
164
165 pub last_chunk_pos: SyncMutex<ChunkPos>,
167 pub last_tracking_view: SyncMutex<Option<PlayerChunkView>>,
169 pub chunk_sender: SyncMutex<ChunkSender>,
171
172 client_information: SyncMutex<ClientInformation>,
175
176 pub chat: SyncMutex<ChatState>,
178
179 game_modes: SyncMutex<PlayerGameModeState>,
181
182 pub inventory: Shared<PlayerInventory>,
184
185 inventory_sync: SyncMutex<PlayerInventorySyncState>,
187
188 last_item_in_main_hand: SyncMutex<ItemStack>,
190
191 inventory_menu: SyncMutex<Menu>,
193
194 open_menu: SyncMutex<player_inventory::OpenMenuState>,
197
198 container_counter: SyncMutex<ContainerCounter>,
200
201 teleport_state: SyncMutex<TeleportState>,
203 item_cooldowns: SyncMutex<ItemCooldowns>,
205
206 tick_state: SyncMutex<PlayerTickState>,
208
209 pub abilities: SyncMutex<Abilities>,
211
212 pub block_breaking: SyncMutex<BlockBreakingManager>,
214
215 living_base: LivingEntityBase,
218
219 pub food_data: SyncMutex<FoodData>,
221
222 health_sync: SyncMutex<HealthSyncState>,
224
225 pub experience: SyncMutex<Experience>,
227
228 permissions: SyncMutex<PlayerPermissionState>,
230
231 seen_credits: SyncMutex<bool>,
233
234 won_game: SyncMutex<bool>,
236
237 pub chunk_send_epoch: SyncMutex<u32>,
240
241 residence: SyncMutex<PlayerResidenceState>,
243 ender_pearls: SyncMutex<Vec<Weak<dyn Entity>>>,
246}
247
248unsafe 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#[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 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 #[must_use]
307 pub fn game_mode(&self) -> GameType {
308 self.game_modes.lock().current()
309 }
310
311 #[must_use]
313 pub fn previous_game_mode(&self) -> Option<GameType> {
314 self.game_modes.lock().previous()
315 }
316
317 pub(crate) fn restore_game_modes(&self, current: GameType, previous: Option<GameType>) {
319 self.game_modes.lock().set_pair(current, previous);
320 }
321
322 fn change_game_mode_state(&self, game_mode: GameType) -> bool {
324 self.game_modes.lock().change_current(game_mode)
325 }
326
327 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 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 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 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 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 }
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 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 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 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 #[expect(clippy::unused_self, reason = "this is an api function")]
590 pub fn handle_custom_payload(&self, _packet: SCustomPayload) {}
591
592 pub fn handle_client_tick_end(&self) {
594 self.movement.lock().finish_client_tick();
595 }
596
597 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 if self.get_health() <= 0.0 {
615 return false;
616 }
617
618 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 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 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 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 let world = self.get_world();
698
699 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 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 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 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 #[expect(clippy::unused_self, reason = "this is an api function")]
777 pub const fn cleanup(&self) {}
778
779 pub fn get_world(&self) -> Arc<World> {
781 self.world.load_full()
782 }
783
784 pub(crate) fn server(&self) -> Arc<Server> {
786 self.server
787 .upgrade()
788 .expect("player must not outlive server")
789 }
790
791 pub(crate) fn domain_residence_token(&self) -> DomainResidenceToken {
793 self.residence.lock().token
794 }
795
796 pub(crate) fn advance_domain_residence(&self) -> DomainResidenceToken {
798 self.residence.lock().advance()
799 }
800
801 pub(crate) fn is_domain_residence_current(&self, token: DomainResidenceToken) -> bool {
803 self.residence.lock().token == token
804 }
805
806 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 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 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 #[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 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 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 #[must_use]
1040 pub fn tick_count(&self) -> i32 {
1041 self.tick_state.lock().tick_count()
1042 }
1043
1044 #[must_use]
1046 pub(crate) fn take_xp_delay(&self) -> i32 {
1047 self.tick_state.lock().take_xp_delay()
1048 }
1049
1050 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 }
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 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 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 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 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;