Skip to main content

steel_core/entity/base/
mod.rs

1//! Common base functionality shared by all entities.
2//!
3//! `EntityBase` contains the core fields and methods that every entity needs.
4//! Entities embed this struct and delegate common `Entity` trait methods to it.
5
6mod fire_freeze;
7mod movement;
8mod persistence;
9mod relationships;
10
11pub use fire_freeze::EntityFireFreezeState;
12pub use movement::{
13    EntityGroundContact, EntityMovement, EntityMovementEmission, EntityMovementFlags,
14    EntityMovementProgress, EntityVerticalMovementStateUpdate,
15};
16pub use persistence::{EntityBaseLoad, EntityBaseSaveData};
17pub use relationships::PendingWorldChangeToken;
18use relationships::{EntityLifecycleState, EntityRelationshipState};
19
20use std::sync::{Arc, Weak};
21
22use glam::DVec3;
23use simdnbt::owned::NbtCompound;
24use smallvec::SmallVec;
25use steel_math::{DEGREE_90, DEGREE_360};
26use steel_registry::entity_data::EntityPose;
27use steel_registry::entity_type::EntityDimensions;
28use steel_registry::vanilla_entities;
29use steel_utils::locks::SyncMutex;
30use steel_utils::{BlockPos, BlockStateId, WorldAabb};
31use text_components::TextComponent;
32use uuid::Uuid;
33
34use crate::entity::damage::DamageHistoryBinding;
35use crate::entity::fluid_contact::EntityFluidContact;
36use crate::entity::{
37    EntityGeneration, EntityLevelCallback, EntityMoveError, InsideBlockEffectType,
38    NullEntityCallback, RemovalReason, SharedEntity,
39};
40use crate::physics::EntityPhysicsState;
41use crate::portal::{PortalKind, PortalProcessResult, PortalProcessor};
42use crate::world::World;
43
44const BOARDING_COOLDOWN: i32 = 60;
45const PISTON_MOVEMENT_LIMIT: f64 = 0.51;
46const PISTON_ZERO_MOVEMENT_EPSILON: f64 = 1.0e-7;
47const PISTON_APPLIED_MOVEMENT_EPSILON: f64 = 1.0e-5;
48const STUCK_SPEED_MULTIPLIER_EPSILON: f64 = 1.0e-7;
49const MOVEMENT_TRACE_LIMIT: usize = 100;
50const MOVEMENT_TRACE_POSITION_EPSILON_SQ: f64 = 9.999_999_4e-11;
51/// Default ticks of powder-snow exposure required before an entity starts
52/// taking freeze damage.
53pub const DEFAULT_TICKS_REQUIRED_TO_FREEZE: i32 = 140;
54/// Default maximum air supply in ticks before an entity starts drowning.
55pub const DEFAULT_MAX_AIR_SUPPLY: i32 = 300;
56/// Maximum number of scoreboard tags a single entity may carry.
57pub const MAX_ENTITY_TAGS: usize = 1024;
58const FIRE_IGNITE_TICKS: i32 = 8 * 20;
59const LAVA_IGNITE_TICKS: i32 = 15 * 20;
60
61fn require_finite_position(position: DVec3, field: &str) {
62    assert!(
63        position.is_finite(),
64        "entity {field} must be finite: {position:?}"
65    );
66}
67
68fn normalize_rotation(rotation: (f32, f32)) -> (f32, f32) {
69    assert!(
70        rotation.0.is_finite() && rotation.1.is_finite(),
71        "entity rotation must be finite: {rotation:?}"
72    );
73    (
74        rotation.0 % DEGREE_360,
75        rotation.1.clamp(-DEGREE_90, DEGREE_90) % DEGREE_360,
76    )
77}
78
79#[derive(Debug, Clone, Copy)]
80pub(crate) struct EntityPhysicsStateInput {
81    pub(crate) max_up_step: f32,
82    pub(crate) backs_off_from_edge: bool,
83    pub(crate) descending: bool,
84    pub(crate) can_walk_on_powder_snow: bool,
85    pub(crate) is_falling_block: bool,
86}
87
88pub(crate) struct EntityPushState {
89    pub(crate) position: DVec3,
90    pub(crate) no_physics: bool,
91    pub(crate) is_vehicle: bool,
92}
93
94#[derive(Debug, Default)]
95struct EntityMovementTrace {
96    // One segment is the common per-tick case. `EntityMovement` is large and both buffers live on
97    // every entity, so larger inline capacities would optimize uncommon multi-movement ticks at a
98    // persistent per-entity memory cost.
99    movement_this_tick: SmallVec<[EntityMovement; 1]>,
100    final_movements_this_tick: SmallVec<[EntityMovement; 1]>,
101}
102
103impl EntityMovementTrace {
104    fn record(&mut self, movement: EntityMovement) {
105        if self.movement_this_tick.len() >= MOVEMENT_TRACE_LIMIT {
106            let first = self.movement_this_tick.remove(0);
107            let second = self.movement_this_tick.remove(0);
108            self.movement_this_tick
109                .insert(0, EntityMovement::new(first.from(), second.to()));
110        }
111
112        self.movement_this_tick.push(movement);
113    }
114
115    fn remove_latest_recording(&mut self) {
116        self.movement_this_tick.pop();
117    }
118
119    fn reset(&mut self) {
120        self.movement_this_tick.clear();
121        self.final_movements_this_tick.clear();
122    }
123
124    fn take_for_block_effects(
125        &mut self,
126        old_position: DVec3,
127        position: DVec3,
128    ) -> SmallVec<[EntityMovement; 1]> {
129        self.final_movements_this_tick.clear();
130        self.final_movements_this_tick
131            .extend(self.movement_this_tick.drain(..));
132
133        if let Some(last_movement) = self.final_movements_this_tick.last().copied() {
134            if (last_movement.to() - position).length_squared() > MOVEMENT_TRACE_POSITION_EPSILON_SQ
135            {
136                self.final_movements_this_tick
137                    .push(EntityMovement::new(last_movement.to(), position));
138            }
139        } else {
140            self.final_movements_this_tick
141                .push(EntityMovement::new(old_position, position));
142        }
143
144        self.final_movements_this_tick.clone()
145    }
146
147    fn last_for_block_effects(&self) -> SmallVec<[EntityMovement; 1]> {
148        self.final_movements_this_tick.clone()
149    }
150}
151
152/// Per-tick piston movement accumulated by vanilla `Entity.limitPistonMovement`.
153#[derive(Debug, Clone, Copy, PartialEq)]
154struct EntityPistonMovement {
155    deltas: [f64; 3],
156    game_time: i64,
157}
158
159impl EntityPistonMovement {
160    const fn new() -> Self {
161        Self {
162            deltas: [0.0; 3],
163            game_time: 0,
164        }
165    }
166
167    fn limit_movement(&mut self, movement: DVec3, current_game_time: i64) -> DVec3 {
168        if movement.length_squared() <= PISTON_ZERO_MOVEMENT_EPSILON {
169            return movement;
170        }
171
172        if current_game_time != self.game_time {
173            self.deltas = [0.0; 3];
174            self.game_time = current_game_time;
175        }
176
177        if movement.x != 0.0 {
178            return self.apply_axis_restriction(0, movement.x, DVec3::X);
179        }
180        if movement.y != 0.0 {
181            return self.apply_axis_restriction(1, movement.y, DVec3::Y);
182        }
183        if movement.z != 0.0 {
184            return self.apply_axis_restriction(2, movement.z, DVec3::Z);
185        }
186
187        DVec3::ZERO
188    }
189
190    fn apply_axis_restriction(&mut self, axis: usize, amount: f64, unit: DVec3) -> DVec3 {
191        let limited =
192            (amount + self.deltas[axis]).clamp(-PISTON_MOVEMENT_LIMIT, PISTON_MOVEMENT_LIMIT);
193        let applied = limited - self.deltas[axis];
194        self.deltas[axis] = limited;
195
196        if applied.abs() <= PISTON_APPLIED_MOVEMENT_EPSILON {
197            DVec3::ZERO
198        } else {
199            unit * applied
200        }
201    }
202}
203
204/// Sound parameters for vanilla amethyst step chimes.
205#[derive(Debug, Clone, Copy, PartialEq)]
206pub struct EntityAmethystStepSound {
207    /// Chime volume.
208    pub volume: f32,
209    /// Chime pitch.
210    pub pitch: f32,
211}
212
213/// Vanilla `Entity` movement state stored as one locked snapshot.
214///
215/// Position, velocity, rotation, and ground contact are commonly read together
216/// by physics, saving, and future navigation code. Keeping them in one struct
217/// makes those ownership boundaries explicit while still exposing focused
218/// accessors through [`EntityBase`].
219#[derive(Debug, Clone, Copy, PartialEq)]
220pub struct EntityBaseState {
221    tick_count: i32,
222    first_tick: bool,
223    position: DVec3,
224    old_position: DVec3,
225    last_known_position: Option<DVec3>,
226    last_known_speed: DVec3,
227    velocity: DVec3,
228    rotation: (f32, f32),
229    old_rotation: (f32, f32),
230    pose: EntityPose,
231    dimensions: EntityDimensions,
232    bounding_box: WorldAabb,
233    movement_flags: EntityMovementFlags,
234    ground_contact: EntityGroundContact,
235    movement_progress: EntityMovementProgress,
236    fire_freeze: EntityFireFreezeState,
237    in_block_state: Option<BlockStateId>,
238    fluid_contact: EntityFluidContact,
239    was_eye_in_water: bool,
240    was_touching_water: bool,
241    piston_movement: EntityPistonMovement,
242    fall_distance: f64,
243    stuck_speed_multiplier: DVec3,
244    no_physics: bool,
245    needs_velocity_sync: bool,
246    hurt_marked: bool,
247    requires_precise_position: bool,
248}
249
250impl EntityBaseState {
251    /// Creates base state for a freshly spawned entity.
252    #[must_use]
253    pub fn new(position: DVec3, dimensions: EntityDimensions) -> Self {
254        require_finite_position(position, "position");
255        Self {
256            tick_count: 0,
257            first_tick: true,
258            position,
259            old_position: position,
260            last_known_position: None,
261            last_known_speed: DVec3::ZERO,
262            velocity: DVec3::ZERO,
263            rotation: (0.0, 0.0),
264            old_rotation: (0.0, 0.0),
265            pose: EntityPose::Standing,
266            dimensions,
267            bounding_box: Self::make_bounding_box(position, dimensions),
268            movement_flags: EntityMovementFlags::new(),
269            ground_contact: EntityGroundContact::airborne(),
270            movement_progress: EntityMovementProgress::new(),
271            fire_freeze: EntityFireFreezeState::new(),
272            in_block_state: None,
273            fluid_contact: EntityFluidContact::default(),
274            was_eye_in_water: false,
275            was_touching_water: false,
276            piston_movement: EntityPistonMovement::new(),
277            fall_distance: 0.0,
278            stuck_speed_multiplier: DVec3::ZERO,
279            no_physics: false,
280            needs_velocity_sync: false,
281            hurt_marked: false,
282            requires_precise_position: false,
283        }
284    }
285
286    /// Creates base state with an explicit bounding box.
287    ///
288    /// Hanging entities and other special cases do not use the default
289    /// dimensions-centered box.
290    #[must_use]
291    pub fn new_with_bounding_box(
292        position: DVec3,
293        dimensions: EntityDimensions,
294        bounding_box: WorldAabb,
295    ) -> Self {
296        Self {
297            bounding_box,
298            ..Self::new(position, dimensions)
299        }
300    }
301
302    #[must_use]
303    fn make_bounding_box(position: DVec3, dimensions: EntityDimensions) -> WorldAabb {
304        WorldAabb::entity_box(
305            position.x,
306            position.y,
307            position.z,
308            f64::from(dimensions.half_width()),
309            f64::from(dimensions.height),
310        )
311    }
312
313    /// Sets velocity on this state snapshot.
314    #[must_use]
315    pub fn with_velocity(mut self, velocity: DVec3) -> Self {
316        if velocity.is_finite() {
317            self.velocity = velocity;
318        }
319        self
320    }
321
322    /// Sets previous position on this state snapshot.
323    #[must_use]
324    pub fn with_old_position(mut self, old_position: DVec3) -> Self {
325        require_finite_position(old_position, "old position");
326        self.old_position = old_position;
327        self
328    }
329
330    /// Sets rotation on this state snapshot.
331    #[must_use]
332    pub fn with_rotation(mut self, rotation: (f32, f32)) -> Self {
333        let rotation = normalize_rotation(rotation);
334        self.rotation = rotation;
335        self.old_rotation = rotation;
336        self
337    }
338
339    /// Sets accumulated fall distance on this state snapshot.
340    #[must_use]
341    pub const fn with_fall_distance(mut self, fall_distance: f64) -> Self {
342        self.fall_distance = fall_distance;
343        self
344    }
345
346    /// Sets base fire/freeze state on this construction snapshot.
347    #[must_use]
348    pub const fn with_fire_freeze_state(mut self, fire_freeze: EntityFireFreezeState) -> Self {
349        self.fire_freeze = fire_freeze;
350        self
351    }
352
353    /// Sets the ground-contact flag on this state snapshot.
354    #[must_use]
355    pub const fn with_on_ground(mut self, on_ground: bool) -> Self {
356        self.movement_flags = self.movement_flags.with_on_ground(on_ground);
357        self.ground_contact = if on_ground {
358            EntityGroundContact::on_ground(None)
359        } else {
360            EntityGroundContact::airborne()
361        };
362        self
363    }
364}
365
366/// Common fields and methods shared by all entities.
367///
368/// Entities embed this struct to avoid duplicating core identity, position,
369/// and lifecycle management code. The `Entity` trait implementation can then
370/// delegate to `EntityBase` methods for common functionality.
371///
372/// # Example
373///
374/// ```ignore
375/// pub struct MyEntity {
376///     base: EntityBase,
377///     // Entity-specific fields...
378/// }
379///
380/// impl Entity for MyEntity {
381///     fn id(&self) -> i32 { self.base.id() }
382///     fn uuid(&self) -> Uuid { self.base.uuid() }
383///     fn position(&self) -> DVec3 { self.base.position() }
384///     // ... delegate other common methods ...
385///
386///     // Entity-specific implementations:
387///     fn entity_type(&self) -> EntityTypeRef { vanilla_entities::MY_ENTITY }
388///     fn tick(&self) { /* custom tick logic */ }
389/// }
390/// ```
391pub struct EntityBase {
392    damage_history: DamageHistoryBinding,
393    /// Generation counter for this runtime construction of the entity.
394    generation: EntityGeneration,
395    /// Unique network ID for this entity (session-local).
396    id: i32,
397    /// Persistent UUID for this entity.
398    uuid: Uuid,
399    /// The world this entity is in.
400    world: SyncMutex<Weak<World>>,
401    /// Current vanilla movement state.
402    state: SyncMutex<EntityBaseState>,
403    /// Shared vanilla save data outside the movement snapshot.
404    save_data: SyncMutex<EntityBaseSaveData>,
405    /// Per-tick movement segments used by vanilla block-contact effects.
406    movement_trace: SyncMutex<EntityMovementTrace>,
407    /// Removal and tick bookkeeping.
408    lifecycle: SyncMutex<EntityLifecycleState>,
409    /// Passenger, vehicle, and boarding-cooldown state.
410    relationships: SyncMutex<EntityRelationshipState>,
411    /// Active vanilla portal timing state.
412    portal_process: SyncMutex<Option<PortalProcessor>>,
413    /// Callback for entity lifecycle events.
414    level_callback: SyncMutex<Arc<dyn EntityLevelCallback>>,
415}
416
417impl EntityBase {
418    /// Creates a new `EntityBase` with a randomly generated UUID.
419    #[must_use]
420    pub fn new(id: i32, position: DVec3, dimensions: EntityDimensions, world: Weak<World>) -> Self {
421        Self::new_with_state(id, EntityBaseState::new(position, dimensions), world)
422    }
423
424    /// Creates a new `EntityBase` with a randomly generated UUID and explicit state.
425    #[must_use]
426    #[expect(
427        clippy::large_types_passed_by_value,
428        reason = "EntityBaseState is an owned construction snapshot built with with_* helpers"
429    )]
430    pub fn new_with_state(id: i32, state: EntityBaseState, world: Weak<World>) -> Self {
431        Self::with_uuid_and_state(id, Uuid::new_v4(), state, world)
432    }
433
434    /// Creates a new `EntityBase` with the specified UUID.
435    ///
436    /// Use this when loading entities from disk or when the UUID is known.
437    #[must_use]
438    pub fn with_uuid(
439        id: i32,
440        uuid: Uuid,
441        position: DVec3,
442        dimensions: EntityDimensions,
443        world: Weak<World>,
444    ) -> Self {
445        Self::with_uuid_and_state(id, uuid, EntityBaseState::new(position, dimensions), world)
446    }
447
448    /// Creates a new `EntityBase` with the specified UUID and restored movement state.
449    ///
450    /// Use this when loading entities from disk so the vanilla base fields are
451    /// reconstructed in one place.
452    #[must_use]
453    #[expect(
454        clippy::large_types_passed_by_value,
455        reason = "EntityBaseState is an owned construction snapshot built with with_* helpers"
456    )]
457    pub fn with_uuid_and_state(
458        id: i32,
459        uuid: Uuid,
460        state: EntityBaseState,
461        world: Weak<World>,
462    ) -> Self {
463        Self {
464            damage_history: DamageHistoryBinding::default(),
465            generation: EntityGeneration::next(),
466            id,
467            uuid,
468            world: SyncMutex::new(world),
469            state: SyncMutex::new(state),
470            save_data: SyncMutex::new(EntityBaseSaveData::new()),
471            movement_trace: SyncMutex::new(EntityMovementTrace::default()),
472            lifecycle: SyncMutex::new(EntityLifecycleState::new()),
473            relationships: SyncMutex::new(EntityRelationshipState::default()),
474            portal_process: SyncMutex::new(None),
475            level_callback: SyncMutex::new(Arc::new(NullEntityCallback)),
476        }
477    }
478
479    /// Creates a base from persistent vanilla entity fields.
480    #[must_use]
481    pub fn from_load(load: EntityBaseLoad, dimensions: EntityDimensions) -> Self {
482        let base = Self::with_uuid_and_state(
483            load.id,
484            load.uuid,
485            EntityBaseState::new(load.position, dimensions)
486                .with_velocity(load.velocity)
487                .with_rotation(load.rotation)
488                .with_fall_distance(load.fall_distance)
489                .with_fire_freeze_state(load.fire_freeze)
490                .with_on_ground(load.on_ground),
491            load.world,
492        );
493        base.replace_save_data(load.save_data);
494        base
495    }
496
497    pub(crate) const fn damage_history(&self) -> &DamageHistoryBinding {
498        &self.damage_history
499    }
500
501    /// Gets the generation counter of this runtime construction of the entity.
502    #[inline]
503    pub const fn generation(&self) -> EntityGeneration {
504        self.generation
505    }
506
507    /// Gets the entity's unique network ID.
508    #[inline]
509    pub const fn id(&self) -> i32 {
510        self.id
511    }
512
513    /// Gets the entity's UUID.
514    #[inline]
515    pub const fn uuid(&self) -> Uuid {
516        self.uuid
517    }
518
519    /// Gets the entity's current position.
520    #[inline]
521    pub fn position(&self) -> DVec3 {
522        self.state.lock().position
523    }
524
525    /// Gets the entity position used by vanilla movement traces.
526    #[inline]
527    pub fn old_position(&self) -> DVec3 {
528        self.state.lock().old_position
529    }
530
531    /// Returns vanilla `lastKnownSpeed`, the displacement computed at base-tick start.
532    #[inline]
533    pub fn known_speed(&self) -> DVec3 {
534        self.state.lock().last_known_speed
535    }
536
537    /// Returns the number of ticks this entity has existed for.
538    #[inline]
539    pub fn tick_count(&self) -> i32 {
540        self.state.lock().tick_count
541    }
542
543    /// Returns whether the entity has not completed its first tick.
544    #[inline]
545    pub fn is_first_tick(&self) -> bool {
546        self.state.lock().first_tick
547    }
548
549    /// Sets whether the entity has not completed its first tick.
550    #[inline]
551    pub fn set_first_tick(&self, first_tick: bool) {
552        self.state.lock().first_tick = first_tick;
553    }
554
555    /// Gets the entity's current bounding box.
556    #[inline]
557    pub fn bounding_box(&self) -> WorldAabb {
558        self.state.lock().bounding_box
559    }
560
561    /// Returns the vanilla movement physics snapshot from the current base state.
562    pub(crate) fn physics_state(&self, input: EntityPhysicsStateInput) -> EntityPhysicsState {
563        let state = self.state.lock();
564        EntityPhysicsState::new(state.position, state.bounding_box, input.max_up_step)
565            .with_on_ground(state.movement_flags.on_ground())
566            .with_backs_off_from_edge(input.backs_off_from_edge)
567            .with_fall_distance(state.fall_distance)
568            .with_descending(input.descending)
569            .with_can_walk_on_powder_snow(input.can_walk_on_powder_snow)
570            .with_falling_block(input.is_falling_block)
571    }
572
573    /// Gets the entity's current pose.
574    #[inline]
575    pub fn pose(&self) -> EntityPose {
576        self.state.lock().pose
577    }
578
579    /// Gets the entity's current dimensions.
580    #[inline]
581    pub fn dimensions(&self) -> EntityDimensions {
582        self.state.lock().dimensions
583    }
584
585    /// Gets the entity's current velocity in blocks per tick.
586    #[inline]
587    pub fn velocity(&self) -> DVec3 {
588        self.state.lock().velocity
589    }
590
591    /// Gets the entity's rotation as (yaw, pitch) in degrees.
592    #[inline]
593    pub fn rotation(&self) -> (f32, f32) {
594        self.state.lock().rotation
595    }
596
597    /// Gets vanilla `yRotO`/`xRotO` as (yaw, pitch) in degrees.
598    #[inline]
599    pub fn old_rotation(&self) -> (f32, f32) {
600        self.state.lock().old_rotation
601    }
602
603    /// Returns true if the entity is touching the ground.
604    #[inline]
605    pub fn on_ground(&self) -> bool {
606        self.state.lock().movement_flags.on_ground()
607    }
608
609    /// Returns the current vanilla movement flag snapshot.
610    #[inline]
611    pub fn movement_flags(&self) -> EntityMovementFlags {
612        self.state.lock().movement_flags
613    }
614
615    /// Returns the current vanilla ground-contact snapshot.
616    #[inline]
617    pub fn ground_contact(&self) -> EntityGroundContact {
618        self.state.lock().ground_contact
619    }
620
621    /// Returns vanilla movement side-effect progress counters.
622    #[inline]
623    pub fn movement_progress(&self) -> EntityMovementProgress {
624        self.state.lock().movement_progress
625    }
626
627    /// Returns the current vanilla fire/freeze state.
628    #[inline]
629    pub fn fire_freeze_state(&self) -> EntityFireFreezeState {
630        self.state.lock().fire_freeze
631    }
632
633    /// Returns a snapshot of shared vanilla save data.
634    pub fn save_data(&self) -> EntityBaseSaveData {
635        self.save_data.lock().clone()
636    }
637
638    /// Replaces shared vanilla save data.
639    pub fn replace_save_data(&self, save_data: EntityBaseSaveData) {
640        *self.save_data.lock() = save_data;
641    }
642
643    /// Returns vanilla `Entity.getInBlockState`, cached until base tick or block-position change.
644    pub fn in_block_state(&self, world: &World) -> BlockStateId {
645        let mut state = self.state.lock();
646        if let Some(in_block_state) = state.in_block_state {
647            return in_block_state;
648        }
649
650        let position = state.position;
651        let block_pos = BlockPos::containing(position.x, position.y, position.z);
652        let in_block_state = world.get_block_state(block_pos);
653        state.in_block_state = Some(in_block_state);
654        in_block_state
655    }
656
657    /// Replaces the current vanilla fire/freeze state.
658    pub fn set_fire_freeze_state(&self, fire_freeze: EntityFireFreezeState) {
659        self.state.lock().fire_freeze = fire_freeze;
660    }
661
662    /// Returns true if the last movement was clipped horizontally.
663    #[inline]
664    pub fn horizontal_collision(&self) -> bool {
665        self.state.lock().movement_flags.horizontal_collision()
666    }
667
668    /// Returns true if the last movement was clipped vertically.
669    #[inline]
670    pub fn vertical_collision(&self) -> bool {
671        self.state.lock().movement_flags.vertical_collision()
672    }
673
674    /// Returns true if the last vertical collision was below the entity.
675    #[inline]
676    pub fn vertical_collision_below(&self) -> bool {
677        self.state.lock().movement_flags.vertical_collision_below()
678    }
679
680    /// Returns the block currently supporting this entity, if known.
681    pub fn supporting_block(&self) -> Option<BlockPos> {
682        self.state.lock().ground_contact.supporting_block()
683    }
684
685    /// Returns true when the entity is grounded but no supporting block was found.
686    pub fn on_ground_no_blocks(&self) -> bool {
687        self.state.lock().ground_contact.on_ground_no_blocks()
688    }
689
690    /// Returns cached fluid contact from the last entity fluid refresh.
691    pub fn fluid_contact(&self) -> EntityFluidContact {
692        self.state.lock().fluid_contact
693    }
694
695    /// Returns vanilla `wasEyeInWater` from the previous fluid refresh.
696    pub fn was_eye_in_water(&self) -> bool {
697        self.state.lock().was_eye_in_water
698    }
699
700    /// Returns whether the last base-tick fluid refresh found water.
701    pub fn was_touching_water(&self) -> bool {
702        self.state.lock().was_touching_water
703    }
704
705    /// Returns accumulated vanilla fall distance.
706    #[inline]
707    pub fn fall_distance(&self) -> f64 {
708        self.state.lock().fall_distance
709    }
710
711    /// Returns true when movement bypasses collision physics.
712    #[inline]
713    pub fn no_physics(&self) -> bool {
714        self.state.lock().no_physics
715    }
716
717    /// Reads the push inputs under one lock, or `None` while riding so the caller
718    /// falls back to the regular push callbacks.
719    pub(crate) fn unmounted_push_state(&self) -> Option<EntityPushState> {
720        let mut relationships = self.relationships.lock();
721
722        if relationships.vehicle().is_some() {
723            return None;
724        }
725
726        let is_vehicle = relationships.first_passenger().is_some();
727        drop(relationships);
728
729        let state = self.state.lock();
730
731        Some(EntityPushState {
732            position: state.position,
733            no_physics: state.no_physics,
734            is_vehicle,
735        })
736    }
737
738    /// Returns this entity's current air supply in ticks.
739    #[inline]
740    pub fn air_supply(&self) -> i32 {
741        self.save_data.lock().air_supply
742    }
743
744    /// Returns the vanilla portal cooldown in ticks.
745    #[inline]
746    pub fn portal_cooldown(&self) -> i32 {
747        self.save_data.lock().portal_cooldown
748    }
749
750    /// Returns whether the entity is on vanilla portal cooldown.
751    #[inline]
752    pub fn is_on_portal_cooldown(&self) -> bool {
753        self.portal_cooldown() > 0
754    }
755
756    /// Returns the active vanilla portal process, if the entity is charging a portal.
757    #[inline]
758    pub fn portal_process(&self) -> Option<PortalProcessor> {
759        *self.portal_process.lock()
760    }
761
762    /// Returns whether this entity ignores gravity.
763    #[inline]
764    pub fn no_gravity(&self) -> bool {
765        self.save_data.lock().no_gravity
766    }
767
768    /// Returns whether this entity is invulnerable to non-bypassing damage.
769    #[inline]
770    pub fn invulnerable(&self) -> bool {
771        self.save_data.lock().invulnerable
772    }
773
774    /// Returns the optional vanilla custom name.
775    #[inline]
776    pub fn custom_name(&self) -> Option<TextComponent> {
777        self.save_data.lock().custom_name.clone()
778    }
779
780    /// Returns the vanilla custom-name visibility flag.
781    #[inline]
782    pub fn custom_name_visible(&self) -> bool {
783        self.save_data.lock().custom_name_visible
784    }
785
786    /// Returns the synchronized vanilla silent flag.
787    #[inline]
788    pub fn silent(&self) -> bool {
789        self.save_data.lock().silent
790    }
791
792    /// Returns the server-owned vanilla glowing tag flag.
793    #[inline]
794    pub fn glowing(&self) -> bool {
795        self.save_data.lock().glowing
796    }
797
798    /// Returns a sorted snapshot of vanilla scoreboard tags.
799    pub fn tags(&self) -> Vec<String> {
800        self.save_data.lock().tags.iter().cloned().collect()
801    }
802
803    /// Returns a snapshot of vanilla custom data.
804    pub fn custom_data(&self) -> NbtCompound {
805        self.save_data.lock().custom_data.clone()
806    }
807
808    /// Returns whether this entity's velocity has changed enough to need
809    /// syncing to clients.
810    #[inline]
811    pub fn needs_velocity_sync(&self) -> bool {
812        self.state.lock().needs_velocity_sync
813    }
814
815    /// Returns true when vanilla hurt-marked velocity sync is pending.
816    #[inline]
817    pub fn hurt_marked(&self) -> bool {
818        self.state.lock().hurt_marked
819    }
820
821    /// Returns true when tracker movement sync must send a full position packet.
822    #[inline]
823    pub fn requires_precise_position(&self) -> bool {
824        self.state.lock().requires_precise_position
825    }
826
827    /// Sets whether tracker movement sync must send a full position packet.
828    pub fn set_requires_precise_position(&self, requires_precise_position: bool) {
829        self.state.lock().requires_precise_position = requires_precise_position;
830    }
831
832    /// Gets the world this entity is in.
833    ///
834    /// Returns `None` if the world has been dropped.
835    #[inline]
836    pub fn level(&self) -> Option<Arc<World>> {
837        self.world.lock().upgrade()
838    }
839
840    /// Gets the vehicle this entity is riding, if it is still loaded.
841    pub fn vehicle(&self) -> Option<SharedEntity> {
842        self.relationships.lock().vehicle()
843    }
844
845    /// Gets this entity's direct passengers, pruning stale weak references.
846    pub fn passengers(&self) -> Vec<SharedEntity> {
847        self.relationships.lock().passengers()
848    }
849
850    /// Gets this entity's first direct passenger, if present.
851    pub fn first_passenger(&self) -> Option<SharedEntity> {
852        self.relationships.lock().first_passenger()
853    }
854
855    /// Returns true when this entity has at least one direct passenger.
856    pub fn is_vehicle(&self) -> bool {
857        self.first_passenger().is_some()
858    }
859
860    /// Returns true when the entity ID is a direct passenger.
861    pub fn has_passenger_id(&self, passenger_id: i32) -> bool {
862        self.relationships.lock().has_passenger_id(passenger_id)
863    }
864
865    /// Returns the vanilla boarding cooldown in ticks.
866    pub fn boarding_cooldown(&self) -> i32 {
867        self.relationships.lock().boarding_cooldown
868    }
869
870    /// Removes a direct passenger by entity ID.
871    pub(crate) fn remove_passenger_id(&self, passenger_id: i32) -> bool {
872        self.relationships.lock().remove_passenger_id(passenger_id)
873    }
874
875    /// Stops riding the current vehicle, if any.
876    pub fn stop_riding(&self) {
877        self.stop_riding_relationship();
878    }
879
880    /// Restores a persisted passenger relationship without applying gameplay boarding rules.
881    pub(crate) fn restore_passenger_relationship(vehicle: &SharedEntity, passenger: &SharedEntity) {
882        passenger.base().stop_riding_relationship();
883        Self::add_passenger_relationship(vehicle, passenger);
884    }
885
886    /// Starts a gameplay passenger relationship after vanilla boarding rules pass.
887    pub(crate) fn start_riding_relationship(vehicle: &SharedEntity, passenger: &SharedEntity) {
888        passenger.base().stop_riding_relationship();
889        Self::add_passenger_relationship(vehicle, passenger);
890    }
891
892    fn add_passenger_relationship(vehicle: &SharedEntity, passenger: &SharedEntity) {
893        if vehicle.base().has_passenger_id(passenger.id()) {
894            return;
895        }
896
897        passenger.base().relationships.lock().vehicle = Some(Arc::downgrade(vehicle));
898        let passenger_ref = Arc::downgrade(passenger);
899        let mut vehicle_relationships = vehicle.base().relationships.lock();
900        let first_passenger_is_player = vehicle_relationships
901            .first_passenger()
902            .is_some_and(|first| first.entity_type() == &vanilla_entities::PLAYER);
903        if passenger.entity_type() == &vanilla_entities::PLAYER && !first_passenger_is_player {
904            vehicle_relationships.passengers.insert(0, passenger_ref);
905        } else {
906            vehicle_relationships.passengers.push(passenger_ref);
907        }
908    }
909
910    /// Sets the vanilla boarding cooldown in ticks.
911    pub(crate) fn set_boarding_cooldown(&self, boarding_cooldown: i32) {
912        self.relationships.lock().boarding_cooldown = boarding_cooldown;
913    }
914
915    /// Advances the base-tick movement and relationship state Steel currently implements.
916    pub fn advance_base_tick_state(&self) {
917        self.clear_in_block_state_for_base_tick();
918        self.set_old_rotation_to_current();
919        self.compute_known_speed();
920        self.decrement_boarding_cooldown();
921    }
922
923    /// Clears vanilla `inBlockState` at the start of base tick.
924    fn clear_in_block_state_for_base_tick(&self) {
925        self.state.lock().in_block_state = None;
926    }
927
928    /// Computes vanilla `lastKnownSpeed` from the previous base-tick position.
929    pub fn compute_known_speed(&self) {
930        let mut state = self.state.lock();
931        let previous_position = match state.last_known_position {
932            Some(position) => position,
933            None => state.position,
934        };
935        state.last_known_speed = state.position - previous_position;
936        state.last_known_position = Some(state.position);
937    }
938
939    fn decrement_boarding_cooldown(&self) {
940        let mut relationships = self.relationships.lock();
941        if relationships.boarding_cooldown > 0 {
942            relationships.boarding_cooldown -= 1;
943        }
944    }
945
946    /// Advances vanilla portal cooldown by one server tick.
947    pub fn process_portal_cooldown(&self) {
948        let mut save_data = self.save_data.lock();
949        if save_data.portal_cooldown > 0 {
950            save_data.portal_cooldown -= 1;
951        }
952    }
953
954    /// Updates the world reference used by this entity.
955    pub(crate) fn set_world(&self, world: Weak<World>) {
956        *self.world.lock() = world;
957    }
958
959    /// Marks this entity as waiting for a prepared world change.
960    pub fn begin_pending_world_change(&self) -> Option<PendingWorldChangeToken> {
961        let mut lifecycle = self.lifecycle.lock();
962        if lifecycle.removal_reason.is_some() || lifecycle.pending_world_change.is_some() {
963            return None;
964        }
965        let token = lifecycle.next_world_change_token();
966        lifecycle.pending_world_change = Some(token);
967        Some(token)
968    }
969
970    /// Marks a live or killed player as waiting for respawn preparation.
971    ///
972    /// Killed players remain eligible because their async spawn search may need
973    /// to be retried after the death animation removes their live entity.
974    pub(crate) fn begin_pending_player_respawn(&self) -> Option<PendingWorldChangeToken> {
975        let mut lifecycle = self.lifecycle.lock();
976        if !matches!(lifecycle.removal_reason, None | Some(RemovalReason::Killed))
977            || lifecycle.pending_world_change.is_some()
978        {
979            return None;
980        }
981        let token = lifecycle.next_world_change_token();
982        lifecycle.pending_world_change = Some(token);
983        Some(token)
984    }
985
986    /// Clears a pending world change if it still matches the provided token.
987    pub fn finish_pending_world_change(&self, token: PendingWorldChangeToken) -> bool {
988        let mut lifecycle = self.lifecycle.lock();
989        if lifecycle.pending_world_change != Some(token) {
990            return false;
991        }
992        lifecycle.pending_world_change = None;
993        true
994    }
995
996    /// Returns true while this entity is waiting for a prepared world change.
997    #[inline]
998    pub fn is_world_change_pending(&self) -> bool {
999        self.lifecycle.lock().pending_world_change.is_some()
1000    }
1001
1002    /// Returns true if the given world-change token is still pending.
1003    #[inline]
1004    pub fn is_world_change_token_pending(&self, token: PendingWorldChangeToken) -> bool {
1005        self.lifecycle.lock().pending_world_change == Some(token)
1006    }
1007
1008    /// Returns true if the entity has been marked for removal.
1009    #[inline]
1010    pub fn is_removed(&self) -> bool {
1011        self.lifecycle.lock().removal_reason.is_some()
1012    }
1013
1014    /// Returns the reason this entity was removed, if it has been removed.
1015    #[inline]
1016    pub fn removal_reason(&self) -> Option<RemovalReason> {
1017        self.lifecycle.lock().removal_reason
1018    }
1019
1020    /// Marks the entity as removed with the given reason.
1021    ///
1022    /// Notifies the level callback on first removal.
1023    pub fn set_removed(&self, reason: RemovalReason) {
1024        let (callback, released) = {
1025            let mut lifecycle = self.lifecycle.lock();
1026            if lifecycle.removal_reason.is_some() {
1027                (None, None)
1028            } else {
1029                lifecycle.removal_reason = Some(reason);
1030                lifecycle.pending_world_change = None;
1031                (
1032                    Some(self.level_callback.lock().clone()),
1033                    self.damage_history.release_on_removal(),
1034                )
1035            }
1036        };
1037        drop(released);
1038
1039        if let Some(callback) = callback {
1040            self.detach_from_relationships(reason);
1041            callback.on_remove(reason);
1042            *self.level_callback.lock() = Arc::new(NullEntityCallback);
1043        }
1044    }
1045
1046    fn detach_from_relationships(&self, reason: RemovalReason) {
1047        if reason.should_destroy() {
1048            self.stop_riding_relationship();
1049        }
1050        self.eject_passenger_relationships();
1051    }
1052
1053    fn stop_riding_relationship(&self) {
1054        let vehicle = {
1055            let mut relationships = self.relationships.lock();
1056            let vehicle = relationships.vehicle();
1057            relationships.vehicle = None;
1058            vehicle
1059        };
1060
1061        if let Some(vehicle) = vehicle {
1062            vehicle.base().remove_passenger_id(self.id);
1063            self.set_boarding_cooldown(BOARDING_COOLDOWN);
1064        }
1065    }
1066
1067    fn eject_passenger_relationships(&self) {
1068        let passengers = {
1069            let mut relationships = self.relationships.lock();
1070            let passengers = relationships.passengers();
1071            relationships.passengers.clear();
1072            passengers
1073        };
1074
1075        for passenger in passengers {
1076            if passenger.base().clear_vehicle_if(self.id) {
1077                passenger.base().set_boarding_cooldown(BOARDING_COOLDOWN);
1078            }
1079        }
1080    }
1081
1082    fn clear_vehicle_if(&self, vehicle_id: i32) -> bool {
1083        {
1084            let mut relationships = self.relationships.lock();
1085            let Some(vehicle) = relationships.vehicle() else {
1086                return false;
1087            };
1088            if vehicle.id() != vehicle_id {
1089                return false;
1090            }
1091        }
1092
1093        if let Err(error) = self.try_set_position(self.position()) {
1094            log::warn!(
1095                "Failed to refresh passenger {} manager position before clearing vehicle {vehicle_id}: {error}",
1096                self.id
1097            );
1098        }
1099
1100        let mut relationships = self.relationships.lock();
1101        let Some(vehicle) = relationships.vehicle() else {
1102            return false;
1103        };
1104        if vehicle.id() != vehicle_id {
1105            return false;
1106        }
1107        relationships.vehicle = None;
1108        true
1109    }
1110
1111    /// Clears the removed flag and returns whether the entity had been removed.
1112    ///
1113    /// Vanilla uses this when an entity instance itself survives a world change.
1114    /// Previously downgraded history stays weak; a new hit can retain a new source.
1115    pub fn clear_removed(&self) -> bool {
1116        let mut lifecycle = self.lifecycle.lock();
1117        let was_removed = lifecycle.removal_reason.is_some();
1118        lifecycle.removal_reason = None;
1119        lifecycle.pending_world_change = None;
1120        self.damage_history.clear_removed();
1121        was_removed
1122    }
1123
1124    /// Sets the level callback for lifecycle events.
1125    pub fn set_level_callback(&self, callback: Arc<dyn EntityLevelCallback>) {
1126        *self.level_callback.lock() = callback;
1127    }
1128
1129    /// Sets the entity's position through the active level callback.
1130    #[must_use = "movement commits can fail when world entity state rejects the update"]
1131    pub fn try_set_position(&self, pos: DVec3) -> Result<(), EntityMoveError> {
1132        require_finite_position(pos, "position");
1133        let old_pos = self.state.lock().position;
1134        let callback = self.level_callback.lock().clone();
1135        callback.validate_move(old_pos, pos)?;
1136        self.set_position_local_unchecked(pos);
1137        if let Err(error) = callback.on_move_committed(old_pos, pos) {
1138            self.set_position_local_unchecked(old_pos);
1139            return Err(error);
1140        }
1141        Ok(())
1142    }
1143
1144    /// Sets position without consulting world lifecycle callbacks.
1145    ///
1146    /// Use this for construction, loading, proto-staged entities, and tests.
1147    pub(crate) fn set_position_local(&self, pos: DVec3) {
1148        let callback = self.level_callback.lock().clone();
1149        assert!(
1150            callback.allows_local_position_update(),
1151            "entity {} local position update bypassed world entity manager",
1152            self.id
1153        );
1154        self.set_position_local_unchecked(pos);
1155    }
1156
1157    fn set_position_local_unchecked(&self, pos: DVec3) {
1158        require_finite_position(pos, "position");
1159        {
1160            let mut state = self.state.lock();
1161            let old = state.position;
1162            state.position = pos;
1163            state.bounding_box = EntityBaseState::make_bounding_box(pos, state.dimensions);
1164            if BlockPos::containing(old.x, old.y, old.z)
1165                != BlockPos::containing(pos.x, pos.y, pos.z)
1166            {
1167                state.in_block_state = None;
1168            }
1169        }
1170    }
1171
1172    /// Sets the vanilla movement-trace old position to the current position.
1173    pub fn set_old_position_to_current(&self) {
1174        let mut state = self.state.lock();
1175        state.old_position = state.position;
1176    }
1177
1178    /// Sets the vanilla movement-trace old position explicitly.
1179    pub fn set_old_position(&self, old_position: DVec3) {
1180        require_finite_position(old_position, "old position");
1181        self.state.lock().old_position = old_position;
1182    }
1183
1184    /// Copies the current rotation into the old-rotation snapshot used for interpolation.
1185    pub fn set_old_rotation_to_current(&self) {
1186        let mut state = self.state.lock();
1187        state.old_rotation = state.rotation;
1188    }
1189
1190    /// Sets vanilla `yRotO` to the current yaw without changing `xRotO`.
1191    pub fn set_old_yaw_to_current(&self) {
1192        let mut state = self.state.lock();
1193        state.old_rotation.0 = state.rotation.0;
1194    }
1195
1196    /// Sets the old-rotation snapshot explicitly, normalizing the angles.
1197    pub fn set_old_rotation(&self, old_rotation: (f32, f32)) {
1198        self.state.lock().old_rotation = normalize_rotation(old_rotation);
1199    }
1200
1201    /// Records a movement segment for vanilla block-contact effects.
1202    pub fn record_movement_this_tick(&self, movement: EntityMovement) {
1203        self.movement_trace.lock().record(movement);
1204    }
1205
1206    /// Removes the latest movement segment recorded this tick.
1207    pub fn remove_latest_movement_recording(&self) {
1208        self.movement_trace.lock().remove_latest_recording();
1209    }
1210
1211    /// Clears movement segments recorded for the current tick.
1212    pub fn clear_movement_this_tick(&self) {
1213        self.movement_trace.lock().reset();
1214    }
1215
1216    /// Takes and finalizes this tick's movement segments for block-contact effects.
1217    pub fn take_movements_for_block_effects(&self) -> SmallVec<[EntityMovement; 1]> {
1218        let (old_position, position) = {
1219            let state = self.state.lock();
1220            (state.old_position, state.position)
1221        };
1222
1223        self.movement_trace
1224            .lock()
1225            .take_for_block_effects(old_position, position)
1226    }
1227
1228    /// Returns the last finalized movement segments for vanilla block-contact effects.
1229    pub fn last_movements_for_block_effects(&self) -> SmallVec<[EntityMovement; 1]> {
1230        self.movement_trace.lock().last_for_block_effects()
1231    }
1232
1233    /// Sets the entity's bounding box directly.
1234    ///
1235    /// Use this for vanilla entities whose box is not simply dimensions centered
1236    /// on the entity position.
1237    pub fn set_bounding_box(&self, bounding_box: WorldAabb) {
1238        self.state.lock().bounding_box = bounding_box;
1239        self.notify_bounding_box_changed(bounding_box);
1240    }
1241
1242    /// Sets pose and dimensions, then rebuilds the default position-centered box.
1243    pub fn set_pose_and_dimensions(&self, pose: EntityPose, dimensions: EntityDimensions) {
1244        let bounding_box = {
1245            let mut state = self.state.lock();
1246            state.pose = pose;
1247            state.dimensions = dimensions;
1248            state.bounding_box = EntityBaseState::make_bounding_box(state.position, dimensions);
1249            state.bounding_box
1250        };
1251        self.notify_bounding_box_changed(bounding_box);
1252    }
1253
1254    fn notify_bounding_box_changed(&self, bounding_box: WorldAabb) {
1255        let callback = self.level_callback.lock().clone();
1256        callback.on_bounding_box_changed(bounding_box);
1257    }
1258
1259    /// Sets the entity's velocity in blocks per tick.
1260    pub fn set_velocity(&self, velocity: DVec3) {
1261        if velocity.is_finite() {
1262            self.state.lock().velocity = velocity;
1263        }
1264    }
1265
1266    /// Vanilla `push(double, double, double)` under one lock: a non-finite impulse is
1267    /// ignored, and a non-finite sum keeps the old velocity like `setDeltaMovement`
1268    /// while still marking the velocity for sync.
1269    pub(crate) fn push_impulse(&self, impulse: DVec3) {
1270        if !impulse.is_finite() {
1271            return;
1272        }
1273
1274        let mut state = self.state.lock();
1275        let velocity = state.velocity + impulse;
1276
1277        if velocity.is_finite() {
1278            state.velocity = velocity;
1279        }
1280
1281        state.needs_velocity_sync = true;
1282    }
1283
1284    /// Advances this entity's tick counter by one.
1285    #[inline]
1286    pub fn advance_tick_count(&self) {
1287        let mut state = self.state.lock();
1288        state.tick_count = state.tick_count.wrapping_add(1);
1289    }
1290
1291    /// Sets this entity's tick counter, for entities that persist it.
1292    #[inline]
1293    pub fn set_tick_count(&self, tick_count: i32) {
1294        self.state.lock().tick_count = tick_count;
1295    }
1296
1297    /// Records movement distance used by vanilla step, swim, and flap effects.
1298    pub fn record_movement_progress(
1299        &self,
1300        clipped_movement: DVec3,
1301        climbing: bool,
1302    ) -> EntityMovementProgress {
1303        let mut state = self.state.lock();
1304        state
1305            .movement_progress
1306            .add_movement(clipped_movement, climbing);
1307        state.movement_progress
1308    }
1309
1310    /// Stores vanilla `nextStep` after a produced movement side effect.
1311    pub fn set_next_step(&self, next_step: f32) {
1312        self.state.lock().movement_progress.next_step = next_step;
1313    }
1314
1315    /// Returns vanilla amethyst-step chime parameters when the cooldown allows it.
1316    pub fn amethyst_step_sound(&self, tick_count: i32) -> Option<EntityAmethystStepSound> {
1317        let intensity = {
1318            let mut state = self.state.lock();
1319            let progress = &mut state.movement_progress;
1320            if tick_count < progress.last_crystal_sound_play_tick + 20 {
1321                return None;
1322            }
1323
1324            let tick_delta = tick_count - progress.last_crystal_sound_play_tick;
1325            progress.crystal_sound_intensity *= 0.997_f32.powi(tick_delta);
1326            progress.crystal_sound_intensity = (progress.crystal_sound_intensity + 0.07).min(1.0);
1327            progress.last_crystal_sound_play_tick = tick_count;
1328            progress.crystal_sound_intensity
1329        };
1330
1331        let pitch = rand::random_range(0.5..0.5 + intensity * 1.2);
1332        let volume = 0.1 + intensity * 1.2;
1333        Some(EntityAmethystStepSound { volume, pitch })
1334    }
1335
1336    /// Sets the entity's rotation as (yaw, pitch) in degrees.
1337    pub fn set_rotation(&self, rotation: (f32, f32)) {
1338        self.state.lock().rotation = normalize_rotation(rotation);
1339    }
1340
1341    /// Sets whether this entity bypasses collision physics.
1342    pub fn set_no_physics(&self, no_physics: bool) {
1343        self.state.lock().no_physics = no_physics;
1344    }
1345
1346    /// Sets this entity's air supply in ticks.
1347    pub fn set_air_supply(&self, air_supply: i32) {
1348        self.save_data.lock().air_supply = air_supply;
1349    }
1350
1351    /// Sets the vanilla portal cooldown in ticks.
1352    pub fn set_portal_cooldown(&self, portal_cooldown: i32) {
1353        self.save_data.lock().portal_cooldown = portal_cooldown;
1354    }
1355
1356    /// Marks this entity as inside a vanilla portal during the current tick.
1357    pub fn set_as_inside_portal(&self, portal: PortalKind, entry_position: BlockPos) {
1358        let mut portal_process = self.portal_process.lock();
1359        match portal_process.as_mut() {
1360            Some(process) if process.is_same_portal(portal) => {
1361                process.set_as_inside_portal(entry_position);
1362            }
1363            _ => {
1364                *portal_process = Some(PortalProcessor::new(portal, entry_position));
1365            }
1366        }
1367    }
1368
1369    /// Advances the active vanilla portal process, if one exists.
1370    pub fn process_portal_teleportation(
1371        &self,
1372        allowed_to_teleport: bool,
1373        transition_time: i32,
1374    ) -> Option<PortalProcessResult> {
1375        self.portal_process.lock().as_mut().map(|process| {
1376            process.process_portal_teleportation(allowed_to_teleport, transition_time)
1377        })
1378    }
1379
1380    /// Replaces active portal timing state during vanilla player restoration.
1381    pub(crate) fn set_portal_process(&self, portal_process: Option<PortalProcessor>) {
1382        *self.portal_process.lock() = portal_process;
1383    }
1384
1385    /// Clears the active vanilla portal process.
1386    pub fn clear_portal_process(&self) {
1387        *self.portal_process.lock() = None;
1388    }
1389
1390    /// Sets whether this entity ignores gravity.
1391    pub fn set_no_gravity(&self, no_gravity: bool) {
1392        self.save_data.lock().no_gravity = no_gravity;
1393    }
1394
1395    /// Sets whether this entity is invulnerable to non-bypassing damage.
1396    pub fn set_invulnerable(&self, invulnerable: bool) {
1397        self.save_data.lock().invulnerable = invulnerable;
1398    }
1399
1400    /// Sets the optional vanilla custom name.
1401    pub fn set_custom_name(&self, custom_name: Option<TextComponent>) {
1402        self.save_data.lock().custom_name = custom_name;
1403    }
1404
1405    /// Sets the vanilla custom-name visibility flag.
1406    pub fn set_custom_name_visible(&self, visible: bool) {
1407        self.save_data.lock().custom_name_visible = visible;
1408    }
1409
1410    /// Sets the synchronized vanilla silent flag.
1411    pub fn set_silent(&self, silent: bool) {
1412        self.save_data.lock().silent = silent;
1413    }
1414
1415    /// Sets the server-owned vanilla glowing tag flag.
1416    pub fn set_glowing(&self, glowing: bool) {
1417        self.save_data.lock().glowing = glowing;
1418    }
1419
1420    /// Adds a vanilla scoreboard tag.
1421    pub fn add_tag(&self, tag: String) -> bool {
1422        self.save_data.lock().add_tag(tag)
1423    }
1424
1425    /// Removes a vanilla scoreboard tag.
1426    pub fn remove_tag(&self, tag: &str) -> bool {
1427        self.save_data.lock().tags.remove(tag)
1428    }
1429
1430    /// Replaces vanilla custom data.
1431    pub fn set_custom_data(&self, custom_data: NbtCompound) {
1432        self.save_data.lock().custom_data = custom_data;
1433    }
1434
1435    /// Marks this entity's velocity as needing to be synced to clients.
1436    pub fn mark_velocity_sync(&self) {
1437        self.state.lock().needs_velocity_sync = true;
1438    }
1439
1440    /// Clears the vanilla velocity sync marker after send processing.
1441    pub fn clear_velocity_sync(&self) {
1442        self.state.lock().needs_velocity_sync = false;
1443    }
1444
1445    /// Marks this entity as hurt for vanilla self-inclusive motion sync.
1446    pub fn mark_hurt(&self) {
1447        self.state.lock().hurt_marked = true;
1448    }
1449
1450    /// Clears the vanilla hurt-marked motion sync flag.
1451    pub fn clear_hurt_mark(&self) {
1452        self.state.lock().hurt_marked = false;
1453    }
1454
1455    /// Sets accumulated vanilla fall distance.
1456    pub fn set_fall_distance(&self, fall_distance: f64) {
1457        self.state.lock().fall_distance = fall_distance;
1458    }
1459
1460    /// Adds vertical movement to accumulated fall distance using vanilla precision.
1461    pub fn accumulate_fall_distance(&self, vertical_movement: f64) {
1462        self.state.lock().fall_distance -= f64::from(vertical_movement as f32);
1463    }
1464
1465    /// Resets accumulated vanilla fall distance.
1466    pub fn reset_fall_distance(&self) {
1467        self.set_fall_distance(0.0);
1468    }
1469
1470    /// Returns the ticks remaining before this entity stops burning.
1471    pub fn remaining_fire_ticks(&self) -> i32 {
1472        self.state.lock().fire_freeze.remaining_fire_ticks()
1473    }
1474
1475    /// Sets the ticks remaining before this entity stops burning.
1476    pub fn set_remaining_fire_ticks(&self, remaining_fire_ticks: i32) {
1477        self.state.lock().fire_freeze.remaining_fire_ticks = remaining_fire_ticks;
1478    }
1479
1480    /// Returns ticks this entity has spent freezing in powder snow.
1481    pub fn ticks_frozen(&self) -> i32 {
1482        self.state.lock().fire_freeze.ticks_frozen()
1483    }
1484
1485    /// Sets ticks this entity has spent freezing in powder snow.
1486    pub fn set_ticks_frozen(&self, ticks_frozen: i32) {
1487        self.state.lock().fire_freeze.ticks_frozen = ticks_frozen;
1488    }
1489
1490    /// Returns whether the entity touched powder snow during the current tick.
1491    pub fn is_in_powder_snow(&self) -> bool {
1492        self.state.lock().fire_freeze.is_in_powder_snow()
1493    }
1494
1495    /// Returns whether the entity touched powder snow during the previous tick.
1496    pub fn was_in_powder_snow(&self) -> bool {
1497        self.state.lock().fire_freeze.was_in_powder_snow()
1498    }
1499
1500    /// Sets whether this entity currently renders as visually on fire.
1501    pub fn set_visual_fire(&self, has_visual_fire: bool) {
1502        self.state.lock().fire_freeze.has_visual_fire = has_visual_fire;
1503    }
1504
1505    /// Returns whether this entity currently renders as visually on fire.
1506    pub fn has_visual_fire(&self) -> bool {
1507        self.state.lock().fire_freeze.has_visual_fire()
1508    }
1509
1510    /// Returns whether the entity is on fire on the server.
1511    pub fn is_on_fire(&self, fire_immune: bool) -> bool {
1512        !fire_immune && self.remaining_fire_ticks() > 0
1513    }
1514
1515    /// Returns whether the entity is freezing.
1516    pub fn is_freezing(&self) -> bool {
1517        self.state.lock().fire_freeze.is_freezing()
1518    }
1519
1520    /// Returns whether the entity has reached full-freeze duration.
1521    pub fn is_fully_frozen(&self, ticks_required_to_freeze: i32) -> bool {
1522        self.state
1523            .lock()
1524            .fire_freeze
1525            .is_fully_frozen(ticks_required_to_freeze)
1526    }
1527
1528    /// Advances vanilla powder-snow contact at the start of base tick.
1529    pub fn advance_powder_snow_contact_for_base_tick(&self) {
1530        let mut state = self.state.lock();
1531        state.fire_freeze.was_in_powder_snow = state.fire_freeze.is_in_powder_snow;
1532        state.fire_freeze.is_in_powder_snow = false;
1533    }
1534
1535    /// Advances vanilla server-side fire tick state.
1536    ///
1537    /// Returns true when the caller should apply one tick of on-fire damage.
1538    pub fn advance_fire_tick(&self, fire_immune: bool, in_lava: bool) -> bool {
1539        let mut state = self.state.lock();
1540        if state.fire_freeze.remaining_fire_ticks <= 0 {
1541            return false;
1542        }
1543
1544        if fire_immune {
1545            state.fire_freeze.remaining_fire_ticks = state.fire_freeze.remaining_fire_ticks.min(0);
1546            return false;
1547        }
1548
1549        let should_damage = state.fire_freeze.remaining_fire_ticks % 20 == 0 && !in_lava;
1550        state.fire_freeze.remaining_fire_ticks -= 1;
1551        should_damage
1552    }
1553
1554    /// Clears accumulated freezing.
1555    pub fn clear_freeze(&self) {
1556        self.set_ticks_frozen(0);
1557    }
1558
1559    /// Clears fire without resetting the vanilla fire immunity cooldown.
1560    pub fn clear_fire(&self) {
1561        let mut state = self.state.lock();
1562        state.fire_freeze.remaining_fire_ticks = state.fire_freeze.remaining_fire_ticks.min(0);
1563    }
1564
1565    /// Ignites this entity for a vanilla tick duration.
1566    pub fn ignite_for_ticks(&self, number_of_ticks: i32, remaining_fire_ticks_cap: Option<i32>) {
1567        let mut state = self.state.lock();
1568        Self::ignite_for_ticks_in_state(
1569            &mut state.fire_freeze,
1570            number_of_ticks,
1571            remaining_fire_ticks_cap,
1572        );
1573    }
1574
1575    /// Applies a vanilla inside-block effect to base fire/freeze state.
1576    pub fn apply_inside_block_effect(
1577        &self,
1578        effect_type: InsideBlockEffectType,
1579        can_freeze: bool,
1580        fire_immune: bool,
1581        fire_ignite_extra_ticks: i32,
1582        ticks_required_to_freeze: i32,
1583        remaining_fire_ticks_cap: Option<i32>,
1584    ) {
1585        let mut state = self.state.lock();
1586        match effect_type {
1587            InsideBlockEffectType::Freeze => {
1588                state.fire_freeze.is_in_powder_snow = true;
1589                if can_freeze {
1590                    state.fire_freeze.ticks_frozen =
1591                        ticks_required_to_freeze.min(state.fire_freeze.ticks_frozen + 1);
1592                }
1593            }
1594            InsideBlockEffectType::ClearFreeze => {
1595                state.fire_freeze.ticks_frozen = 0;
1596            }
1597            InsideBlockEffectType::FireIgnite => {
1598                Self::apply_fire_ignite(
1599                    &mut state.fire_freeze,
1600                    fire_immune,
1601                    fire_ignite_extra_ticks,
1602                    remaining_fire_ticks_cap,
1603                );
1604            }
1605            InsideBlockEffectType::LavaIgnite => {
1606                if !fire_immune {
1607                    Self::ignite_for_ticks_in_state(
1608                        &mut state.fire_freeze,
1609                        LAVA_IGNITE_TICKS,
1610                        remaining_fire_ticks_cap,
1611                    );
1612                }
1613            }
1614            InsideBlockEffectType::Extinguish => {
1615                state.fire_freeze.remaining_fire_ticks =
1616                    state.fire_freeze.remaining_fire_ticks.min(0);
1617            }
1618        }
1619    }
1620
1621    fn apply_fire_ignite(
1622        fire_freeze: &mut EntityFireFreezeState,
1623        fire_immune: bool,
1624        fire_ignite_extra_ticks: i32,
1625        remaining_fire_ticks_cap: Option<i32>,
1626    ) {
1627        if fire_immune {
1628            return;
1629        }
1630
1631        if fire_freeze.remaining_fire_ticks < 0 {
1632            Self::set_remaining_fire_ticks_in_state(
1633                fire_freeze,
1634                fire_freeze.remaining_fire_ticks + 1,
1635                remaining_fire_ticks_cap,
1636            );
1637        } else if fire_ignite_extra_ticks > 0 {
1638            Self::set_remaining_fire_ticks_in_state(
1639                fire_freeze,
1640                fire_freeze.remaining_fire_ticks + fire_ignite_extra_ticks,
1641                remaining_fire_ticks_cap,
1642            );
1643        }
1644
1645        if fire_freeze.remaining_fire_ticks >= 0 {
1646            Self::ignite_for_ticks_in_state(
1647                fire_freeze,
1648                FIRE_IGNITE_TICKS,
1649                remaining_fire_ticks_cap,
1650            );
1651        }
1652    }
1653
1654    fn ignite_for_ticks_in_state(
1655        fire_freeze: &mut EntityFireFreezeState,
1656        number_of_ticks: i32,
1657        remaining_fire_ticks_cap: Option<i32>,
1658    ) {
1659        if fire_freeze.remaining_fire_ticks < number_of_ticks {
1660            Self::set_remaining_fire_ticks_in_state(
1661                fire_freeze,
1662                number_of_ticks,
1663                remaining_fire_ticks_cap,
1664            );
1665        }
1666        fire_freeze.ticks_frozen = 0;
1667    }
1668
1669    fn set_remaining_fire_ticks_in_state(
1670        fire_freeze: &mut EntityFireFreezeState,
1671        remaining_fire_ticks: i32,
1672        remaining_fire_ticks_cap: Option<i32>,
1673    ) {
1674        fire_freeze.remaining_fire_ticks =
1675            Self::cap_remaining_fire_ticks(remaining_fire_ticks, remaining_fire_ticks_cap);
1676    }
1677
1678    fn cap_remaining_fire_ticks(
1679        remaining_fire_ticks: i32,
1680        remaining_fire_ticks_cap: Option<i32>,
1681    ) -> i32 {
1682        remaining_fire_ticks_cap.map_or(remaining_fire_ticks, |cap| remaining_fire_ticks.min(cap))
1683    }
1684
1685    /// Applies vanilla base-tick fall-distance damping while touching lava.
1686    pub fn dampen_fall_distance_in_lava(&self) {
1687        let mut state = self.state.lock();
1688        if !state.first_tick && state.fluid_contact.lava_height() > 0.0 {
1689            state.fall_distance *= 0.5;
1690        }
1691    }
1692
1693    /// Applies vanilla fluid-interaction fall-distance reset while touching water.
1694    pub fn reset_fall_distance_in_water(&self) {
1695        let mut state = self.state.lock();
1696        if state.fluid_contact.water_height() > 0.0 {
1697            state.fall_distance = 0.0;
1698        }
1699    }
1700
1701    /// Sets whether this entity is touching the ground.
1702    pub fn set_on_ground(&self, on_ground: bool) {
1703        let mut state = self.state.lock();
1704        state.movement_flags = state.movement_flags.with_on_ground(on_ground);
1705        if !on_ground {
1706            state.ground_contact = EntityGroundContact::airborne();
1707        }
1708    }
1709
1710    /// Sets all vanilla movement flags after `Entity.move`.
1711    pub fn set_movement_flags(
1712        &self,
1713        movement_flags: EntityMovementFlags,
1714        ground_contact: EntityGroundContact,
1715    ) {
1716        let mut state = self.state.lock();
1717        state.movement_flags = movement_flags;
1718        state.ground_contact = ground_contact;
1719    }
1720
1721    /// Stores the current vanilla supporting-block snapshot.
1722    pub fn set_ground_contact(&self, ground_contact: EntityGroundContact) {
1723        self.state.lock().ground_contact = ground_contact;
1724    }
1725
1726    /// Stores the current vanilla fluid contact snapshot.
1727    pub fn set_fluid_contact(&self, fluid_contact: EntityFluidContact) {
1728        self.state.lock().fluid_contact = fluid_contact;
1729    }
1730
1731    /// Returns whether the entity is currently touching lava.
1732    #[inline]
1733    pub fn is_in_lava(&self) -> bool {
1734        let state = self.state.lock();
1735        !state.first_tick && state.fluid_contact.lava_height() > 0.0
1736    }
1737
1738    /// Records eye-water history from the cached fluid contact. Vanilla does
1739    /// this only in `baseTick`, before refreshing fluid interaction.
1740    pub fn update_was_eye_in_water(&self) {
1741        let mut state = self.state.lock();
1742        state.was_eye_in_water = state.fluid_contact.eye_in_water();
1743    }
1744
1745    /// Stores fluid contact from a vanilla `updateFluidInteraction` refresh,
1746    /// including `wasTouchingWater`.
1747    pub fn set_fluid_interaction(&self, fluid_contact: EntityFluidContact) {
1748        let mut state = self.state.lock();
1749        state.was_touching_water = fluid_contact.water_height() > 0.0;
1750        state.fluid_contact = fluid_contact;
1751    }
1752
1753    /// Sets ground and horizontal collision flags from an accepted client move.
1754    pub fn set_on_ground_with_movement(
1755        &self,
1756        on_ground: bool,
1757        horizontal_collision: bool,
1758        ground_contact: EntityGroundContact,
1759    ) {
1760        let mut state = self.state.lock();
1761        state.movement_flags = state
1762            .movement_flags
1763            .with_on_ground(on_ground)
1764            .with_horizontal_collision(horizontal_collision);
1765        state.ground_contact = ground_contact;
1766    }
1767
1768    /// Clears collision flags after a no-physics move.
1769    pub fn clear_collision_flags(&self) {
1770        let mut state = self.state.lock();
1771        state.movement_flags = state.movement_flags.without_collisions();
1772    }
1773
1774    /// Applies vanilla per-tick piston movement accumulation.
1775    pub fn limit_piston_movement(&self, movement: DVec3, current_game_time: i64) -> DVec3 {
1776        self.state
1777            .lock()
1778            .piston_movement
1779            .limit_movement(movement, current_game_time)
1780    }
1781
1782    /// Sets the speed multiplier used for the next stuck-in-block movement pass.
1783    pub fn make_stuck_in_block(&self, speed_multiplier: DVec3) {
1784        let mut state = self.state.lock();
1785        state.fall_distance = 0.0;
1786        state.stuck_speed_multiplier = speed_multiplier;
1787    }
1788
1789    /// Applies and clears vanilla stuck-in-block speed state.
1790    #[must_use]
1791    pub fn consume_stuck_speed_multiplier(&self, movement: DVec3, apply_multiplier: bool) -> DVec3 {
1792        let mut state = self.state.lock();
1793        if state.stuck_speed_multiplier.length_squared() <= STUCK_SPEED_MULTIPLIER_EPSILON {
1794            return movement;
1795        }
1796
1797        let stuck_speed_multiplier = state.stuck_speed_multiplier;
1798        state.stuck_speed_multiplier = DVec3::ZERO;
1799        state.velocity = DVec3::ZERO;
1800
1801        if apply_multiplier {
1802            movement * stuck_speed_multiplier
1803        } else {
1804            movement
1805        }
1806    }
1807}
1808
1809#[cfg(test)]
1810mod tests;