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