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