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