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