Skip to main content

steel_core/entity/
mod.rs

1//! This module contains entity-related traits and types.
2
3use std::{
4    any::try_as_dyn,
5    borrow::Cow,
6    sync::{Arc, LazyLock, Weak},
7};
8
9use glam::DVec3;
10use rand::{SeedableRng as _, rngs::StdRng};
11use rustc_hash::FxHashSet;
12use simdnbt::borrow::NbtCompound as BorrowedNbtCompoundView;
13use simdnbt::owned::{NbtCompound, NbtList, NbtTag};
14use steel_protocol::packets::game::{
15    AnimateAction, AttributeSnapshot, CAnimate, CDamageEvent, CEntityEvent, CHurtAnimation,
16    CTeleportEntity, EquipmentSlotItem, RelativeMovement, SoundSource,
17};
18use steel_registry::blocks::{
19    behavior::PushReaction, block_state_ext::BlockStateExt as _, properties::BlockStateProperties,
20    shapes::is_shape_full_block,
21};
22use steel_registry::data_components::vanilla_components::{
23    GLIDER, SWING_ANIMATION, SwingAnimation,
24};
25use steel_registry::enchantment_effect::EnchantmentEffectComponent;
26use steel_registry::entity_data::{DataValue, EntityPose, HumanoidArm};
27use steel_registry::entity_type::{EntityAttachment, EntityDimensions, EntityTypeRef};
28use steel_registry::fluid::{FluidState, FluidStateExt as _};
29use steel_registry::game_events::GameEventRef;
30use steel_registry::item_stack::ItemStack;
31use steel_registry::items::ItemRef;
32use steel_registry::loot_table::{
33    DamageSourceInfo, EntityRef, EntityRefFlags, LootContext, LootTableRef,
34};
35use steel_registry::mob_effect::MobEffectRef;
36use steel_registry::sound_event::SoundEventRef;
37use steel_registry::vanilla_block_tags::BlockTag;
38use steel_registry::vanilla_blocks;
39use steel_registry::vanilla_entities;
40use steel_registry::vanilla_entity_type_tags::EntityTypeTag;
41use steel_registry::vanilla_game_rules::{MAX_ENTITY_CRAMMING, MOB_DROPS};
42use steel_registry::vanilla_item_tags::ItemTag;
43use steel_registry::{
44    REGISTRY, TaggedRegistryExt, sound_events, vanilla_damage_type_tags, vanilla_damage_types,
45    vanilla_game_events,
46};
47use steel_registry::{RegistryEntry, RegistryExt};
48use steel_registry::{vanilla_attributes, vanilla_fluid_tags, vanilla_items, vanilla_mob_effects};
49use steel_utils::entity_events::EntityStatus;
50use steel_utils::locks::SyncMutex;
51use steel_utils::types::{Difficulty, InteractionHand};
52use steel_utils::{
53    BlockPos, BlockStateId, ChunkPos, Direction, Downcast as _, ErasedType, Identifier,
54    UuidExt as _, WorldAabb, axis::Axis, block_util::FoundRectangle, text::DisplayResolutor,
55};
56use text_components::{
57    Modifier as _, TextComponent, interactivity::HoverEvent, translation::TranslatedMessage,
58};
59use uuid::Uuid;
60
61use crate::behavior::{
62    BLOCK_BEHAVIORS, BlockCollisionContext, EntityFallOnContext, EntityLandingContext,
63    FLUID_BEHAVIORS, InteractionResult, blocks::PowderSnowBlock,
64};
65use crate::chunk_saver::ChunkStorage;
66use crate::entity::attribute::{AttributeMap, AttributeModifier, AttributeModifierOperation};
67use crate::fluid::{LavaFluid, get_fluid_state, get_height};
68use crate::inventory::equipment::EquipmentSlot;
69use crate::physics::{
70    COLLISION_EPSILON, CollisionWorld, EntityPhysicsState, MoveResult, MoverType,
71    WorldCollisionProvider, move_entity as resolve_entity_movement,
72};
73use crate::world::game_event::GameEventContext;
74use crate::world::{ClipBlockShape, ClipFluid, LevelReader, World};
75use crate::{enchantment_helper, entity::damage::DamageSource, player::Player};
76
77use entities::ExperienceOrbEntity;
78
79fn nbt_bool(value: bool) -> NbtTag {
80    NbtTag::Byte(i8::from(value))
81}
82
83fn entity_type_name(entity_type: EntityTypeRef) -> TextComponent {
84    TextComponent::translated(TranslatedMessage {
85        key: Cow::Owned(format!(
86            "entity.{}.{}",
87            entity_type.key.namespace, entity_type.key.path
88        )),
89        fallback: None,
90        args: None,
91    })
92}
93
94fn remove_entity_name_actions(mut component: TextComponent) -> TextComponent {
95    fn remove_actions(component: &mut TextComponent) {
96        component.interactions.click = None;
97        for child in &mut component.children {
98            remove_actions(child);
99        }
100    }
101
102    remove_actions(&mut component);
103    component
104}
105
106/// Global counter for allocating unique entity IDs.
107///
108/// Mirrors vanilla's `Entity.ENTITY_COUNTER`. Each new entity increments this
109/// counter to get a unique network ID. Starts at 1 (0 is reserved).
110static ENTITY_COUNTER: LazyLock<SyncMutex<i32>> = LazyLock::new(|| SyncMutex::new(1));
111const MOVEMENT_RECORD_EPSILON: f64 = 1.0e-7;
112const NO_PHYSICS_COLLISION_EPSILON: f64 = 1.0e-7;
113const IN_WALL_EYE_BOX_HEIGHT: f64 = 1.0e-6;
114const WATER_ENTITY_FLOW_SCALE: f64 = 0.014;
115const BUBBLE_COLUMN_INSIDE_DOWN_MIN_SPEED: f64 = -0.3;
116const BUBBLE_COLUMN_INSIDE_UP_MAX_SPEED: f64 = 0.7;
117const BUBBLE_COLUMN_ABOVE_DOWN_MIN_SPEED: f64 = -0.9;
118const BUBBLE_COLUMN_ABOVE_UP_MAX_SPEED: f64 = 1.8;
119const BUBBLE_COLUMN_DOWN_ACCELERATION: f64 = 0.03;
120const BUBBLE_COLUMN_INSIDE_UP_ACCELERATION: f64 = 0.06;
121const BUBBLE_COLUMN_ABOVE_UP_ACCELERATION: f64 = 0.1;
122const DAMAGE_KNOCKBACK_POWER: f64 = 0.4_f32 as f64;
123const KNOCKBACK_DIRECTION_EPSILON_SQ: f64 = 1.0e-5_f32 as f64;
124const MOVE_TOWARDS_CLOSEST_SPACE_DIRECTIONS: [Direction; 5] = [
125    Direction::North,
126    Direction::South,
127    Direction::West,
128    Direction::East,
129    Direction::Up,
130];
131
132const fn should_apply_entity_cramming_damage(
133    max_cramming: i32,
134    pushable_count: usize,
135    non_passenger_count: usize,
136    random_roll: i32,
137) -> bool {
138    if max_cramming <= 0 || random_roll != 0 {
139        return false;
140    }
141
142    let threshold = (max_cramming - 1) as usize;
143    pushable_count > threshold && non_passenger_count > threshold
144}
145const LEASH_SCAN_SIZE: f64 = 32.0;
146const LEASH_SCAN_HALF_SIZE: f64 = LEASH_SCAN_SIZE / 2.0;
147const SPEED_MODIFIER_POWDER_SNOW_ID: Identifier = Identifier::vanilla_static("powder_snow");
148
149#[derive(Debug, Clone, Copy, PartialEq, Eq)]
150pub(crate) struct SwimmingEnvironment {
151    pub(crate) sprinting: bool,
152    pub(crate) passenger: bool,
153    pub(crate) in_water: bool,
154    pub(crate) under_water: bool,
155    pub(crate) block_fluid_is_water: bool,
156}
157
158#[must_use]
159pub(crate) const fn select_swimming_state(
160    currently_swimming: bool,
161    env: SwimmingEnvironment,
162) -> bool {
163    if env.passenger {
164        return false;
165    }
166
167    if currently_swimming {
168        env.sprinting && env.in_water
169    } else {
170        env.sprinting && env.under_water && env.block_fluid_is_water
171    }
172}
173
174fn horizontal_distance(vector: DVec3) -> f64 {
175    vector.x.hypot(vector.z)
176}
177
178const fn world_aabb_center(aabb: WorldAabb) -> DVec3 {
179    DVec3::new(
180        f64::midpoint(aabb.min_x(), aabb.max_x()),
181        f64::midpoint(aabb.min_y(), aabb.max_y()),
182        f64::midpoint(aabb.min_z(), aabb.max_z()),
183    )
184}
185
186fn leash_scan_area(center: DVec3) -> WorldAabb {
187    WorldAabb::new(
188        center.x - LEASH_SCAN_HALF_SIZE,
189        center.y - LEASH_SCAN_HALF_SIZE,
190        center.z - LEASH_SCAN_HALF_SIZE,
191        center.x + LEASH_SCAN_HALF_SIZE,
192        center.y + LEASH_SCAN_HALF_SIZE,
193        center.z + LEASH_SCAN_HALF_SIZE,
194    )
195}
196
197fn transfer_leashables_to_holder(leashables: Vec<SharedEntity>, new_holder: &SharedEntity) -> bool {
198    let mut transferred = false;
199    for leashable in leashables {
200        let Some(mob) = leashable.as_mob() else {
201            continue;
202        };
203        if mob.can_have_a_leash_attached_to(new_holder.as_ref()) {
204            let _ = mob.set_leashed_to(new_holder);
205            transferred = true;
206        }
207    }
208    transferred
209}
210
211fn fall_flying_collision_damage(previous_horizontal_speed: f64, new_horizontal_speed: f64) -> f32 {
212    ((previous_horizontal_speed - new_horizontal_speed) * 10.0 - 3.0) as f32
213}
214
215fn entity_eye_suffocation_box(eye_pos: DVec3, width: f64) -> WorldAabb {
216    let half_width = width * 0.5;
217    let half_height = IN_WALL_EYE_BOX_HEIGHT * 0.5;
218    WorldAabb::new(
219        eye_pos.x - half_width,
220        eye_pos.y - half_height,
221        eye_pos.z - half_width,
222        eye_pos.x + half_width,
223        eye_pos.y + half_height,
224        eye_pos.z + half_width,
225    )
226}
227
228fn block_state_suffocates_eye_box(
229    state: BlockStateId,
230    world: &dyn LevelReader,
231    pos: BlockPos,
232    eye_box: WorldAabb,
233) -> bool {
234    if state.is_air() || !state.is_suffocating() {
235        return false;
236    }
237
238    let behavior = BLOCK_BEHAVIORS.get_behavior(state.get_block());
239    behavior
240        .get_collision_shape(state, world, pos, BlockCollisionContext::empty())
241        .iter()
242        .copied()
243        .any(|shape| eye_box.intersects(shape.at_block(pos)))
244}
245
246const fn fall_flying_free_fall_interval(fall_flying_ticks: i32) -> Option<i32> {
247    let check_fall_flying_ticks = fall_flying_ticks.wrapping_add(1);
248    if check_fall_flying_ticks % 10 == 0 {
249        Some(check_fall_flying_ticks / 10)
250    } else {
251        None
252    }
253}
254
255pub(crate) fn equipment_items_to_packet_items(
256    items: Vec<(EquipmentSlot, ItemStack)>,
257) -> Vec<EquipmentSlotItem> {
258    items
259        .into_iter()
260        .map(|(slot, item_stack)| EquipmentSlotItem { slot, item_stack })
261        .collect()
262}
263
264fn aabb_contains_any_liquid(world: &Arc<World>, aabb: WorldAabb) -> bool {
265    (aabb.min_x().floor() as i32..aabb.max_x().ceil() as i32).any(|x| {
266        (aabb.min_y().floor() as i32..aabb.max_y().ceil() as i32).any(|y| {
267            (aabb.min_z().floor() as i32..aabb.max_z().ceil() as i32)
268                .any(|z| !get_fluid_state(world, BlockPos::new(x, y, z)).is_empty())
269        })
270    })
271}
272
273enum BlockEffectSegmentResult {
274    Complete(i32),
275    IterationLimit,
276    Removed,
277}
278
279#[derive(Debug, Clone, Copy)]
280struct BlockEffectFireSnapshot {
281    was_on_fire: bool,
282    was_freezing: bool,
283    previous_remaining_fire_ticks: i32,
284}
285
286impl BlockEffectFireSnapshot {
287    fn from_entity(entity: &dyn Entity) -> Self {
288        Self {
289            was_on_fire: entity.is_on_fire(),
290            was_freezing: entity.is_freezing(),
291            previous_remaining_fire_ticks: entity.remaining_fire_ticks(),
292        }
293    }
294}
295
296fn finish_inside_block_effects(
297    entity: &dyn Entity,
298    effect_collector: &mut InsideBlockEffectCollector,
299    before_effects: BlockEffectFireSnapshot,
300) {
301    effect_collector.apply_and_clear(entity);
302    if entity.is_removed() {
303        return;
304    }
305
306    if is_in_rain(entity) {
307        entity.clear_fire();
308    }
309
310    let extinguished = before_effects.was_on_fire && !entity.is_on_fire()
311        || before_effects.was_freezing && !entity.is_freezing();
312    if extinguished {
313        entity.play_entity_on_fire_extinguished_sound();
314    }
315
316    let ignited_this_tick =
317        entity.remaining_fire_ticks() > before_effects.previous_remaining_fire_ticks;
318    if !entity.is_on_fire() && !ignited_this_tick {
319        entity.set_remaining_fire_ticks(-entity.fire_immune_ticks());
320    } else {
321        entity.sync_base_fire_freeze_entity_data();
322    }
323}
324
325fn is_in_rain(entity: &dyn Entity) -> bool {
326    let Some(world) = entity.level() else {
327        return false;
328    };
329
330    let pos = entity.block_position();
331    world.is_raining_at(pos)
332        || world.is_raining_at(BlockPos::new(
333            pos.x(),
334            entity.bounding_box().max_y().floor() as i32,
335            pos.z(),
336        ))
337}
338
339fn closest_open_space_direction(
340    block_pos: BlockPos,
341    fractional_position: DVec3,
342    mut is_full_collision_block: impl FnMut(BlockPos) -> bool,
343) -> Direction {
344    let mut closest_direction = Direction::Up;
345    let mut closest_distance = f64::MAX;
346
347    for direction in MOVE_TOWARDS_CLOSEST_SPACE_DIRECTIONS {
348        let neighbor_pos = direction.relative(block_pos);
349        if is_full_collision_block(neighbor_pos) {
350            continue;
351        }
352
353        let axis_delta = axis_component(fractional_position, direction.axis());
354        let oriented_delta = if direction_step(direction) > 0.0 {
355            1.0 - axis_delta
356        } else {
357            axis_delta
358        };
359
360        if oriented_delta < closest_distance {
361            closest_distance = oriented_delta;
362            closest_direction = direction;
363        }
364    }
365
366    closest_direction
367}
368
369const fn axis_component(vector: DVec3, axis: Axis) -> f64 {
370    match axis {
371        Axis::X => vector.x,
372        Axis::Y => vector.y,
373        Axis::Z => vector.z,
374    }
375}
376
377const fn direction_step(direction: Direction) -> f64 {
378    match direction {
379        Direction::Down | Direction::North | Direction::West => -1.0,
380        Direction::Up | Direction::South | Direction::East => 1.0,
381    }
382}
383
384fn fall_damage_reset_clip_target(
385    position: DVec3,
386    movement: DVec3,
387    fall_distance: f64,
388) -> Option<DVec3> {
389    if fall_distance == 0.0 || movement.length_squared() < 1.0 {
390        return None;
391    }
392
393    let check_distance = movement.length().min(8.0);
394    Some(position + movement.normalize() * check_distance)
395}
396
397fn trapdoor_usable_as_ladder_state(
398    trapdoor_state: BlockStateId,
399    below_state: BlockStateId,
400) -> bool {
401    if trapdoor_state.try_get_value(&BlockStateProperties::OPEN) != Some(true) {
402        return false;
403    }
404
405    below_state.get_block() == &vanilla_blocks::LADDER
406        && below_state.try_get_value(&BlockStateProperties::FACING)
407            == trapdoor_state.try_get_value(&BlockStateProperties::FACING)
408}
409
410pub(crate) fn get_input_vector(input: DVec3, speed: f32, yaw_degrees: f32) -> DVec3 {
411    if input.length_squared() < 1.0E-7 {
412        return DVec3::ZERO;
413    }
414
415    let movement = if input.length_squared() > 1.0 {
416        input.normalize()
417    } else {
418        input
419    } * f64::from(speed);
420    let yaw = yaw_degrees.to_radians();
421    let sin = yaw.sin();
422    let cos = yaw.cos();
423    DVec3::new(
424        movement.x * f64::from(cos) - movement.z * f64::from(sin),
425        movement.y,
426        movement.z * f64::from(cos) + movement.x * f64::from(sin),
427    )
428}
429
430fn collided_with_fluid(
431    world: &Arc<World>,
432    fluid_state: FluidState,
433    block_pos: BlockPos,
434    from: DVec3,
435    to: DVec3,
436    entity: &dyn Entity,
437) -> bool {
438    if fluid_state.is_empty() {
439        return false;
440    }
441
442    let fluid_height = f64::from(get_height(world, block_pos, fluid_state));
443    let fluid_box = WorldAabb::new(
444        f64::from(block_pos.x()),
445        f64::from(block_pos.y()),
446        f64::from(block_pos.z()),
447        f64::from(block_pos.x() + 1),
448        f64::from(block_pos.y()) + fluid_height,
449        f64::from(block_pos.z() + 1),
450    );
451
452    block_effects::collided_with_aabb_moving_from(
453        entity.make_bounding_box_at(from),
454        from,
455        to,
456        fluid_box,
457    )
458}
459
460fn physics_state_for_move(entity: &dyn Entity) -> EntityPhysicsState {
461    entity.base().physics_state(base::EntityPhysicsStateInput {
462        max_up_step: entity.max_up_step(),
463        backs_off_from_edge: entity.backs_off_from_edge(),
464        descending: entity.is_descending(),
465        can_walk_on_powder_snow: PowderSnowBlock::can_entity_walk_on_powder_snow(entity),
466        is_falling_block: entity.entity_type() == &vanilla_entities::FALLING_BLOCK,
467    })
468}
469
470/// Allocates a new unique entity ID.
471///
472/// This is the primary way to get entity IDs for spawning entities.
473/// Thread-safe through the shared counter lock.
474#[must_use]
475pub fn next_entity_id() -> i32 {
476    let mut counter = ENTITY_COUNTER.lock();
477    let id = *counter;
478    *counter = counter.wrapping_add(1);
479    id
480}
481
482fn apply_block_effect_segment(
483    entity: &dyn Entity,
484    world: &Arc<World>,
485    from: DVec3,
486    to: DVec3,
487    max_iterations: i32,
488    effect_collector: &mut InsideBlockEffectCollector,
489    visited_blocks: &mut FxHashSet<BlockPos>,
490) -> BlockEffectSegmentResult {
491    let aabb = entity.make_bounding_box_at(to).deflate(1.0E-5);
492    if aabb.is_empty() {
493        return BlockEffectSegmentResult::Complete(0);
494    }
495
496    let mut hit_iteration_limit = false;
497    let Some(iterations) =
498        block_effects::for_each_block_intersected_between(from, to, aabb, |pos, iteration| {
499            if entity.is_removed() {
500                return false;
501            }
502            if iteration >= max_iterations {
503                hit_iteration_limit = true;
504                return false;
505            }
506
507            let state = world.get_block_state(pos);
508            if state.is_air() {
509                return true;
510            }
511
512            let behavior = BLOCK_BEHAVIORS.get_behavior(state.get_block());
513            let fluid_state = state.get_fluid_state();
514            let entity_inside_shape =
515                behavior.get_entity_inside_collision_shape(state, world.as_ref(), pos, entity);
516            let inside_block = block_effects::collided_with_shape_moving_from(
517                entity.make_bounding_box_at(from),
518                from,
519                to,
520                pos,
521                entity_inside_shape,
522            );
523            let inside_fluid = collided_with_fluid(world, fluid_state, pos, from, to, entity);
524
525            if !(inside_block || inside_fluid) || !visited_blocks.insert(pos) {
526                return true;
527            }
528
529            if inside_block {
530                let moved_far = from.distance_squared(to) > 0.999_990_000_000_252_6_f64.powi(2);
531                let is_precise = moved_far || aabb.intersects_block(pos);
532                effect_collector.advance_step(iteration);
533                behavior.entity_inside(state, world, pos, entity, effect_collector, is_precise);
534                if entity.is_removed() {
535                    return false;
536                }
537            }
538
539            if inside_fluid {
540                effect_collector.advance_step(iteration);
541                FLUID_BEHAVIORS
542                    .get_behavior(fluid_state.fluid_id)
543                    .entity_inside(world, pos, entity, effect_collector);
544            }
545            !entity.is_removed()
546        })
547    else {
548        if entity.is_removed() {
549            return BlockEffectSegmentResult::Removed;
550        }
551        return if hit_iteration_limit {
552            BlockEffectSegmentResult::IterationLimit
553        } else {
554            BlockEffectSegmentResult::Complete(0)
555        };
556    };
557
558    if entity.is_removed() {
559        BlockEffectSegmentResult::Removed
560    } else {
561        BlockEffectSegmentResult::Complete(iterations)
562    }
563}
564
565fn relative_on_axis(position: DVec3, axis: Axis, amount: f64) -> DVec3 {
566    match axis {
567        Axis::X => DVec3::new(position.x + amount, position.y, position.z),
568        Axis::Y => DVec3::new(position.x, position.y + amount, position.z),
569        Axis::Z => DVec3::new(position.x, position.y, position.z + amount),
570    }
571}
572
573/// Matches vanilla `LivingEntity.resetForwardDirectionOfRelativePortalPosition`.
574#[must_use]
575pub(crate) const fn reset_forward_direction_of_relative_portal_position(offsets: DVec3) -> DVec3 {
576    DVec3::new(offsets.x, offsets.y, 0.0)
577}
578
579fn record_movement_for_block_effects(
580    entity: &dyn Entity,
581    from: DVec3,
582    to: DVec3,
583    requested_movement: DVec3,
584    actual_movement: DVec3,
585) {
586    if should_apply_resolved_movement(requested_movement, actual_movement) {
587        entity.base().record_movement_this_tick(
588            EntityMovement::with_axis_dependent_original_movement(from, to, requested_movement),
589        );
590    }
591}
592
593fn should_apply_resolved_movement(requested_movement: DVec3, actual_movement: DVec3) -> bool {
594    let movement_length = actual_movement.length_squared();
595    movement_length > MOVEMENT_RECORD_EPSILON
596        || requested_movement.length_squared() - movement_length < MOVEMENT_RECORD_EPSILON
597}
598
599fn apply_step_on_block(entity: &dyn Entity, world: &Arc<World>) {
600    if !entity.on_ground() {
601        return;
602    }
603
604    let Some(effect_pos) = entity.on_pos_legacy() else {
605        return;
606    };
607    let effect_state = world.get_block_state(effect_pos);
608    let behavior = BLOCK_BEHAVIORS.get_behavior(effect_state.get_block());
609    behavior.step_on(effect_state, world, effect_pos, entity);
610}
611
612#[expect(
613    clippy::too_many_lines,
614    reason = "vanilla movement block-effect traversal is easier to audit when kept in one sweep"
615)]
616fn apply_effects_from_block_movements(entity: &dyn Entity, movements: &[EntityMovement]) {
617    if !entity.is_affected_by_blocks() {
618        return;
619    }
620
621    let Some(world) = entity.level() else {
622        return;
623    };
624
625    apply_step_on_block(entity, &world);
626
627    let mut visited_blocks = FxHashSet::default();
628    let mut effect_collector = InsideBlockEffectCollector::new();
629    let before_effects = BlockEffectFireSnapshot::from_entity(entity);
630    for movement in movements.iter().copied() {
631        let mut remaining_iterations = 16;
632        let delta = movement.to() - movement.from();
633        if let Some(original_movement) = movement.axis_dependent_original_movement()
634            && delta.length_squared() > 0.0
635        {
636            let mut segment_from = movement.from();
637            for axis in block_effects::axis_step_order(original_movement) {
638                let axis_move = block_effects::component(delta, axis);
639                if axis_move == 0.0 {
640                    continue;
641                }
642
643                let segment_to = relative_on_axis(segment_from, axis, axis_move);
644                match apply_block_effect_segment(
645                    entity,
646                    &world,
647                    segment_from,
648                    segment_to,
649                    remaining_iterations,
650                    &mut effect_collector,
651                    &mut visited_blocks,
652                ) {
653                    BlockEffectSegmentResult::Complete(iterations) => {
654                        remaining_iterations -= iterations;
655                    }
656                    BlockEffectSegmentResult::IterationLimit => {
657                        apply_block_effect_segment(
658                            entity,
659                            &world,
660                            movement.to(),
661                            movement.to(),
662                            1,
663                            &mut effect_collector,
664                            &mut visited_blocks,
665                        );
666                        finish_inside_block_effects(entity, &mut effect_collector, before_effects);
667                        return;
668                    }
669                    BlockEffectSegmentResult::Removed => {
670                        finish_inside_block_effects(entity, &mut effect_collector, before_effects);
671                        return;
672                    }
673                }
674                segment_from = segment_to;
675            }
676        } else {
677            match apply_block_effect_segment(
678                entity,
679                &world,
680                movement.from(),
681                movement.to(),
682                remaining_iterations,
683                &mut effect_collector,
684                &mut visited_blocks,
685            ) {
686                BlockEffectSegmentResult::Complete(iterations) => {
687                    remaining_iterations -= iterations;
688                }
689                BlockEffectSegmentResult::IterationLimit => {
690                    apply_block_effect_segment(
691                        entity,
692                        &world,
693                        movement.to(),
694                        movement.to(),
695                        1,
696                        &mut effect_collector,
697                        &mut visited_blocks,
698                    );
699                    finish_inside_block_effects(entity, &mut effect_collector, before_effects);
700                    return;
701                }
702                BlockEffectSegmentResult::Removed => {
703                    finish_inside_block_effects(entity, &mut effect_collector, before_effects);
704                    return;
705                }
706            }
707        }
708
709        if remaining_iterations <= 0 {
710            apply_block_effect_segment(
711                entity,
712                &world,
713                movement.to(),
714                movement.to(),
715                1,
716                &mut effect_collector,
717                &mut visited_blocks,
718            );
719            finish_inside_block_effects(entity, &mut effect_collector, before_effects);
720            return;
721        }
722    }
723
724    finish_inside_block_effects(entity, &mut effect_collector, before_effects);
725}
726
727mod ageable;
728pub(crate) mod ai;
729mod animal;
730pub mod attribute;
731mod base;
732mod block_effects;
733mod callback;
734mod combat_rules;
735pub mod damage;
736pub mod entities;
737#[expect(
738    clippy::module_inception,
739    reason = "the entity module mirrors vanilla's Entity class and groups its implementation"
740)]
741mod entity;
742mod fluid_contact;
743#[expect(warnings)]
744#[rustfmt::skip]
745#[path = "generated/entities.rs"]
746mod generated_entities;
747mod inside_block_effects;
748mod item_based_steering;
749mod item_frame;
750mod living_base;
751mod living_entity;
752mod manager;
753mod mob;
754mod movement_sync;
755pub mod projectile;
756mod registry;
757mod spawn;
758mod storage;
759mod synced_data;
760mod ticking;
761mod tracker;
762
763use crate::portal::{
764    PortalKind, PortalProcessResult, PortalProcessor, PortalTicketTarget, TeleportPostAction,
765    TeleportTransition, WorldChangeRequest, portal_shape::PortalShape,
766};
767pub(crate) use ageable::{AgeableMob, AgeableMobBase};
768pub(crate) use animal::{Animal, AnimalBase};
769pub use base::{
770    DEFAULT_MAX_AIR_SUPPLY, DEFAULT_TICKS_REQUIRED_TO_FREEZE, EntityAmethystStepSound, EntityBase,
771    EntityBaseLoad, EntityBaseSaveData, EntityBaseState, EntityFireFreezeState,
772    EntityGroundContact, EntityMovement, EntityMovementEmission, EntityMovementFlags,
773    EntityMovementProgress, EntityVerticalMovementStateUpdate, MAX_ENTITY_TAGS,
774    PendingWorldChangeToken,
775};
776pub use callback::{
777    EntityChunkCallback, EntityLevelCallback, InactiveEntityCallback, NullEntityCallback,
778    PlayerEntityCallback, RemovalReason,
779};
780pub(crate) use entity::apply_entity_look_at;
781pub use entity::{
782    AcceptedClientMovement, AcceptedClientMovementOutcome, Entity, EntityEventSource,
783};
784pub use fluid_contact::EntityFluidContact;
785pub use inside_block_effects::{
786    InsideBlockEffectCallback, InsideBlockEffectCollector, InsideBlockEffectType,
787};
788pub(crate) use item_based_steering::{ItemBasedSteering, ItemSteerable};
789pub use item_frame::ItemFrame;
790pub use living_base::{
791    ActiveMobEffect, DEATH_DURATION, DEFAULT_SWING_DURATION, LivingEntityBase, LivingRotationState,
792    LivingSwingState, LivingTravelInput, MobEffectInstance, MobEffectSyncChange,
793    MobEffectSyncPacket,
794};
795pub use living_entity::LivingEntity;
796pub use manager::{
797    AddEntityError, ChunkEntityLoadResult, EntityLifecycleChanges, EntityMoveError,
798    EntityMoveUpdate, EntityOwnership, EntityVisibility, WorldEntityManager,
799};
800pub(crate) use mob::{Mob, MobBase, PathfinderMob};
801pub use movement_sync::{
802    EntityMovementSyncPacket, EntityMovementSyncPackets, EntityMovementSyncState,
803    EntityMovementSyncUpdate, EntityPositionRotSyncPacket, EntityPositionSyncDecision,
804    EntityPositionSyncPacket, EntityPositionSyncSnapshot, EntityPositionSyncState,
805    EntityRotationSyncState, EntityVelocitySyncState, POSITION_SYNC_THRESHOLD,
806    PackedEntityRotation, ServerEntityMovementSyncState, ServerEntityMovementSyncUpdate,
807};
808pub use projectile::{
809    EntityHitResult, Projectile, ProjectileBase, ProjectileDeflection, ProjectileEventSource,
810    ProjectileHit, ThrowableItemProjectile, ThrowableProjectile, compute_margin,
811};
812pub use registry::{ENTITIES, EntityLoadRequest, EntityRegistry, init_entities};
813pub(crate) use spawn::{AgeableMobGroupData, EntitySpawnReason, SpawnGroupData};
814pub(crate) use storage::{EntityStorage, EntityStorageAddResult};
815pub use synced_data::EntitySyncedData;
816pub(crate) use ticking::{
817    snapshot_old_pos_and_rot_for_tick, tick_vehicle_passengers_with_ticked_if,
818};
819pub use tracker::{EntityChangeSenders, EntityTracker};
820
821#[cfg(test)]
822macro_rules! impl_test_downcast_type {
823    ($type:ty) => {
824        // SAFETY: A fully qualified test module path plus its local type name is
825        // unique within the test process.
826        unsafe impl steel_utils::DowncastType for $type {
827            const TYPE_KEY: steel_utils::DowncastTypeKey = steel_utils::DowncastTypeKey::new(
828                concat!("steel:test/", module_path!(), "/", stringify!($type)),
829            );
830        }
831    };
832}
833
834#[cfg(test)]
835pub(crate) use impl_test_downcast_type;
836
837/// Type alias for a shared entity reference.
838pub type SharedEntity = Arc<dyn Entity>;
839
840/// Type alias for a weak entity reference.
841pub type WeakEntity = Weak<dyn Entity>;
842
843/// The point on an entity used by commands that resolve positions or facing.
844#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
845pub enum EntityAnchor {
846    /// The entity's base position.
847    #[default]
848    Feet,
849    /// The entity's eye position for its current pose.
850    Eyes,
851}
852
853impl EntityAnchor {
854    /// Resolves this anchor against an entity's current position.
855    #[must_use]
856    pub fn position(self, entity: &dyn Entity) -> DVec3 {
857        let position = entity.position();
858        match self {
859            Self::Feet => position,
860            Self::Eyes => DVec3::new(position.x, entity.get_eye_y(), position.z),
861        }
862    }
863}
864
865pub(crate) fn start_riding_entities(
866    passenger: &SharedEntity,
867    entity_to_ride: &SharedEntity,
868) -> bool {
869    if !entity_to_ride.could_accept_passenger() {
870        return false;
871    }
872
873    if !entity_to_ride.entity_type().can_serialize {
874        return false;
875    }
876
877    if entity_to_ride.id() == passenger.id() {
878        return false;
879    }
880
881    let mut vehicle_entity = entity_to_ride.vehicle();
882    while let Some(vehicle) = vehicle_entity {
883        if vehicle.id() == passenger.id() {
884            return false;
885        }
886        vehicle_entity = vehicle.vehicle();
887    }
888
889    if !passenger.can_ride(entity_to_ride.as_ref())
890        || !entity_to_ride.can_add_passenger(passenger.as_ref())
891    {
892        return false;
893    }
894
895    if passenger.is_passenger() {
896        passenger.stop_riding();
897    }
898
899    passenger.set_pose(EntityPose::Standing);
900    EntityBase::start_riding_relationship(entity_to_ride, passenger);
901    // TODO: Emit ENTITY_MOUNT game event and riding advancement trigger once those foundations exist.
902    true
903}
904
905pub(crate) fn change_entity_world(
906    entity: SharedEntity,
907    teleport_transition: &TeleportTransition,
908) -> Option<SharedEntity> {
909    if entity.is_removed() {
910        return None;
911    }
912
913    let source_world = entity.level()?;
914    if source_world.domain() != teleport_transition.target_world.domain() {
915        tracing::error!(
916            entity_id = entity.id(),
917            source_domain = source_world.domain(),
918            target_domain = teleport_transition.target_world.domain(),
919            "Refusing direct cross-domain entity teleport"
920        );
921        return None;
922    }
923
924    if entity.as_player().is_some() {
925        let Some(player) = source_world.players.get_by_entity_id(entity.id()) else {
926            tracing::error!(
927                entity_id = entity.id(),
928                "Refusing player world change for an unregistered player entity"
929            );
930            return None;
931        };
932        // Vanilla `ServerPlayer.teleport` keeps the live player/connection identity
933        // and sends respawn/player-position packets instead of recreating from entity NBT.
934        if !player.change_world_within_domain(teleport_transition) {
935            return None;
936        }
937        return Some(entity);
938    }
939
940    change_non_player_entity_world(entity, teleport_transition)
941}
942
943fn change_non_player_entity_world(
944    entity: SharedEntity,
945    teleport_transition: &TeleportTransition,
946) -> Option<SharedEntity> {
947    if entity.is_removed() {
948        return None;
949    }
950
951    let Some(source_world) = entity.level() else {
952        tracing::warn!(
953            entity_id = entity.id(),
954            entity_type = ?entity.entity_type().key,
955            "Ignoring world change for entity without a live world"
956        );
957        return None;
958    };
959    if source_world.domain() != teleport_transition.target_world.domain() {
960        tracing::error!(
961            entity_id = entity.id(),
962            source_domain = source_world.domain(),
963            target_domain = teleport_transition.target_world.domain(),
964            "Refusing cross-domain non-player entity transition"
965        );
966        return None;
967    }
968
969    entity.set_portal_cooldown(teleport_transition.portal_cooldown);
970    if !teleport_transition.as_passenger {
971        entity.stop_riding();
972    }
973
974    if Arc::ptr_eq(&source_world, &teleport_transition.target_world) {
975        teleport_entity_same_world(entity, teleport_transition)
976    } else {
977        teleport_entity_cross_world(entity, teleport_transition)
978    }
979}
980
981fn teleport_entity_same_world(
982    entity: SharedEntity,
983    teleport_transition: &TeleportTransition,
984) -> Option<SharedEntity> {
985    for passenger in entity.passengers() {
986        let passenger_transition =
987            passenger_transition(entity.as_ref(), passenger.as_ref(), teleport_transition);
988        change_entity_world(passenger, &passenger_transition);
989    }
990
991    if let Err(error) = teleport_set_position(
992        entity.as_ref(),
993        teleport_transition,
994        TeleportPositionCommit::Managed,
995    ) {
996        tracing::warn!(
997            entity_id = entity.id(),
998            entity_type = ?entity.entity_type().key,
999            position = ?teleport_transition.position,
1000            "Failed to commit same-world portal teleport for entity: {error}"
1001        );
1002        return None;
1003    }
1004
1005    if !teleport_transition.as_passenger {
1006        send_teleport_transition_to_riding_players(entity.as_ref(), teleport_transition);
1007    }
1008    apply_post_teleport_transition(entity.as_ref(), teleport_transition);
1009    Some(entity)
1010}
1011
1012fn send_teleport_transition_to_riding_players(
1013    entity: &dyn Entity,
1014    teleport_transition: &TeleportTransition,
1015) {
1016    let controller_id = entity
1017        .controlling_passenger()
1018        .map(|controller| controller.id());
1019    for passenger in indirect_passengers(entity) {
1020        let Some(player) = passenger.as_player() else {
1021            continue;
1022        };
1023        let packet = if Some(passenger.id()) == controller_id {
1024            CTeleportEntity::new(
1025                entity.id(),
1026                teleport_transition.position,
1027                teleport_transition.velocity,
1028                teleport_transition.rotation.0,
1029                teleport_transition.rotation.1,
1030                teleport_transition.relatives,
1031                entity.on_ground(),
1032            )
1033        } else {
1034            let rotation = entity.rotation();
1035            CTeleportEntity::new(
1036                entity.id(),
1037                entity.position(),
1038                entity.velocity(),
1039                rotation.0,
1040                rotation.1,
1041                RelativeMovement::NONE,
1042                entity.on_ground(),
1043            )
1044        };
1045        player.send_packet(packet);
1046    }
1047}
1048
1049fn indirect_passengers(entity: &dyn Entity) -> Vec<SharedEntity> {
1050    fn collect(
1051        passengers: Vec<SharedEntity>,
1052        visited: &mut FxHashSet<i32>,
1053        output: &mut Vec<SharedEntity>,
1054    ) {
1055        for passenger in passengers {
1056            if !visited.insert(passenger.id()) {
1057                continue;
1058            }
1059            output.push(Arc::clone(&passenger));
1060            collect(passenger.passengers(), visited, output);
1061        }
1062    }
1063
1064    let mut visited = FxHashSet::default();
1065    visited.insert(entity.id());
1066    let mut passengers = Vec::new();
1067    collect(entity.passengers(), &mut visited, &mut passengers);
1068    passengers
1069}
1070
1071fn teleport_entity_cross_world(
1072    entity: SharedEntity,
1073    teleport_transition: &TeleportTransition,
1074) -> Option<SharedEntity> {
1075    let position = teleport_transition.resolved_position(entity.position());
1076    let target_chunk = ChunkPos::from_entity_pos(position);
1077    if !teleport_transition
1078        .target_world
1079        .has_full_chunk(target_chunk)
1080    {
1081        tracing::warn!(
1082            entity_id = entity.id(),
1083            entity_type = ?entity.entity_type().key,
1084            chunk = ?target_chunk,
1085            "Ignoring dimension transition for entity because target chunk is not loaded"
1086        );
1087        return None;
1088    }
1089
1090    let old_passengers = entity.passengers();
1091    let mut new_passengers = Vec::with_capacity(old_passengers.len());
1092    for passenger in old_passengers {
1093        passenger.stop_riding();
1094        let passenger_transition =
1095            passenger_transition(entity.as_ref(), passenger.as_ref(), teleport_transition);
1096        if let Some(new_passenger) = change_entity_world(passenger, &passenger_transition) {
1097            new_passengers.push(new_passenger);
1098        }
1099    }
1100
1101    let projectile_owner = entity.projectile_owner();
1102    let Some(persistent) = ChunkStorage::entity_to_dimension_transition_persistent(&entity) else {
1103        tracing::warn!(
1104            entity_id = entity.id(),
1105            entity_type = ?entity.entity_type().key,
1106            "Failed to serialize entity for dimension transition"
1107        );
1108        return None;
1109    };
1110
1111    let target_level = Arc::downgrade(&teleport_transition.target_world);
1112    let mut new_entities = ChunkStorage::persistent_to_entity_tree_at_level(
1113        &persistent,
1114        ChunkPos::from_entity_pos(entity.position()),
1115        &target_level,
1116    );
1117    let Some(new_entity) = new_entities.drain(..).next() else {
1118        tracing::warn!(
1119            entity_id = entity.id(),
1120            entity_type = ?entity.entity_type().key,
1121            "Failed to recreate entity for dimension transition"
1122        );
1123        return None;
1124    };
1125    if let Some(owner) = &projectile_owner {
1126        new_entity.restore_owner_reference(owner);
1127    }
1128
1129    if let Err(error) = teleport_set_position(
1130        new_entity.as_ref(),
1131        teleport_transition,
1132        TeleportPositionCommit::Local,
1133    ) {
1134        tracing::warn!(
1135            entity_id = entity.id(),
1136            entity_type = ?entity.entity_type().key,
1137            position = ?teleport_transition.position,
1138            "Failed to stage dimension transition position for entity: {error}"
1139        );
1140        return None;
1141    }
1142
1143    if let Err(error) = teleport_transition
1144        .target_world
1145        .try_add_entity(Arc::clone(&new_entity))
1146    {
1147        tracing::warn!(
1148            entity_id = entity.id(),
1149            new_entity_id = new_entity.id(),
1150            entity_type = ?new_entity.entity_type().key,
1151            position = ?new_entity.position(),
1152            "Failed to register dimension-transition entity: {error}"
1153        );
1154        new_entity.set_removed(RemovalReason::Discarded);
1155        return None;
1156    }
1157    if new_entity.entity_type() == &vanilla_entities::ENDER_PEARL
1158        && let Some(owner) = &projectile_owner
1159        && let Some(player) = owner.as_player()
1160    {
1161        player.register_ender_pearl(&new_entity);
1162    }
1163
1164    remove_after_changing_dimensions(entity.as_ref());
1165    entity.set_removed(RemovalReason::ChangedWorld);
1166    for new_passenger in new_passengers {
1167        EntityBase::restore_passenger_relationship(&new_entity, &new_passenger);
1168    }
1169
1170    apply_post_teleport_transition(new_entity.as_ref(), teleport_transition);
1171    Some(new_entity)
1172}
1173
1174#[derive(Clone, Copy)]
1175enum TeleportPositionCommit {
1176    Managed,
1177    Local,
1178}
1179
1180fn teleport_set_position(
1181    entity: &dyn Entity,
1182    teleport_transition: &TeleportTransition,
1183    commit: TeleportPositionCommit,
1184) -> Result<(), EntityMoveError> {
1185    let position = teleport_transition.resolved_position(entity.position());
1186    let current_rotation = entity.rotation();
1187    let current_velocity = entity.velocity();
1188    let rotation = teleport_transition.resolved_rotation(current_rotation);
1189    let velocity =
1190        teleport_transition.resolved_velocity(current_velocity, current_rotation, rotation);
1191
1192    match commit {
1193        TeleportPositionCommit::Managed => entity.try_set_position(position)?,
1194        TeleportPositionCommit::Local => entity.base().set_position_local(position),
1195    }
1196    entity.set_rotation(rotation);
1197    if let Some(living) = entity.as_living_entity() {
1198        living.set_y_head_rot(rotation.0);
1199    }
1200    entity.set_old_position_to_current();
1201    entity.base().set_old_rotation_to_current();
1202    entity.set_velocity(velocity);
1203    entity.base().clear_movement_this_tick();
1204    Ok(())
1205}
1206
1207fn passenger_transition(
1208    vehicle: &dyn Entity,
1209    passenger: &dyn Entity,
1210    teleport_transition: &TeleportTransition,
1211) -> TeleportTransition {
1212    let rotation = passenger_transition_rotation(
1213        teleport_transition.rotation,
1214        teleport_transition.relatives,
1215        vehicle.rotation(),
1216        passenger.rotation(),
1217    );
1218    let position = passenger_transition_position(
1219        teleport_transition.position,
1220        teleport_transition.relatives,
1221        vehicle.position(),
1222        passenger.position(),
1223    );
1224
1225    TeleportTransition {
1226        target_world: teleport_transition.target_world.clone(),
1227        position,
1228        rotation,
1229        velocity: teleport_transition.velocity,
1230        relatives: teleport_transition.relatives,
1231        portal_cooldown: teleport_transition.portal_cooldown,
1232        as_passenger: true,
1233        post_transition: teleport_transition.post_transition.clone(),
1234    }
1235}
1236
1237fn passenger_transition_rotation(
1238    transition_rotation: (f32, f32),
1239    relatives: RelativeMovement,
1240    vehicle_rotation: (f32, f32),
1241    passenger_rotation: (f32, f32),
1242) -> (f32, f32) {
1243    let yaw = transition_rotation.0
1244        + if relatives.is_y_rot_relative() {
1245            0.0
1246        } else {
1247            passenger_rotation.0 - vehicle_rotation.0
1248        };
1249    let pitch = transition_rotation.1
1250        + if relatives.is_x_rot_relative() {
1251            0.0
1252        } else {
1253            passenger_rotation.1 - vehicle_rotation.1
1254        };
1255    (yaw, pitch)
1256}
1257
1258fn passenger_transition_position(
1259    transition_position: DVec3,
1260    relatives: RelativeMovement,
1261    vehicle_position: DVec3,
1262    passenger_position: DVec3,
1263) -> DVec3 {
1264    let offset = passenger_position - vehicle_position;
1265    transition_position
1266        + DVec3::new(
1267            if relatives.is_x_relative() {
1268                0.0
1269            } else {
1270                offset.x
1271            },
1272            if relatives.is_y_relative() {
1273                0.0
1274            } else {
1275                offset.y
1276            },
1277            if relatives.is_z_relative() {
1278                0.0
1279            } else {
1280                offset.z
1281            },
1282        )
1283}
1284
1285fn apply_post_teleport_transition(entity: &dyn Entity, teleport_transition: &TeleportTransition) {
1286    for action in teleport_transition.post_transition.actions() {
1287        match *action {
1288            TeleportPostAction::PlayPortalSound => {}
1289            TeleportPostAction::PlacePortalTicket(target) => {
1290                let Some(world) = entity.level() else {
1291                    continue;
1292                };
1293                let ticket_position = match target {
1294                    PortalTicketTarget::Destination => BlockPos::from(entity.position()),
1295                    PortalTicketTarget::Block(pos) => pos,
1296                };
1297                world.place_portal_ticket(ticket_position);
1298            }
1299        }
1300    }
1301}
1302
1303fn remove_after_changing_dimensions(entity: &dyn Entity) {
1304    let Some(mob) = entity.as_mob() else {
1305        return;
1306    };
1307
1308    mob.remove_leash();
1309    for slot in EquipmentSlot::ALL {
1310        mob.living_base()
1311            .equipment()
1312            .lock()
1313            .set(slot, ItemStack::empty());
1314    }
1315}
1316
1317pub(crate) fn entity_loot_ref(entity: &dyn Entity) -> EntityRef<'_> {
1318    let living_entity = entity.as_living_entity();
1319    EntityRef {
1320        entity_type: Some(&entity.entity_type().key),
1321        flags: EntityRefFlags {
1322            is_on_fire: entity.is_on_fire(),
1323            is_sneaking: entity.is_crouching(),
1324            is_sprinting: living_entity.is_some_and(LivingEntity::is_sprinting),
1325            is_swimming: entity.is_swimming(),
1326            is_baby: living_entity.is_some_and(LivingEntity::is_baby),
1327        },
1328        // TODO: Include equipment and custom name once loot contexts can snapshot entity data.
1329        equipment: None,
1330        custom_name: None,
1331    }
1332}
1333
1334#[cfg(test)]
1335mod tests;