Skip to main content

steel_core/entity/
mod.rs

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