1use 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
84pub(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
129static 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#[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#[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 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
874pub type SharedEntity = Arc<dyn Entity>;
876
877pub type WeakEntity = Weak<dyn Entity>;
879
880#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
882pub enum EntityAnchor {
883 #[default]
885 Feet,
886 Eyes,
888}
889
890impl EntityAnchor {
891 #[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 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 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 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
1375pub 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
1391pub(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;