steel_core/entity/
dismount_helper.rs1use std::sync::Arc;
4
5use glam::DVec3;
6use steel_registry::blocks::{
7 block_state_ext::BlockStateExt,
8 properties::BlockStateProperties,
9 shapes::{OffsetVoxelShape, VoxelShape},
10};
11use steel_registry::entity_data::EntityPose;
12use steel_registry::vanilla_block_tags::BlockTag;
13use steel_registry::{vanilla_blocks, vanilla_entities};
14use steel_utils::{BlockPos, BlockStateId, WorldAabb, axis::Axis};
15
16use crate::behavior::{BLOCK_BEHAVIORS, BlockCollisionContext};
17use crate::entity::Entity;
18use crate::entity::ai::walk::WalkPathEvaluator;
19use crate::physics::{CollisionWorld, WorldCollisionProvider};
20use crate::world::World;
21
22#[must_use]
23#[expect(
24 dead_code,
25 reason = "vanilla DismountHelper foundation; vehicle dismounts use this next"
26)]
27pub(crate) const fn offsets_for_direction(forward: steel_utils::Direction) -> [(i32, i32); 8] {
28 let right = forward.rotate_y_clockwise();
29 let left = right.opposite();
30 let back = forward.opposite();
31 let (right_x, right_z) = right.offset_xz();
32 let (left_x, left_z) = left.offset_xz();
33 let (back_x, back_z) = back.offset_xz();
34 let (forward_x, forward_z) = forward.offset_xz();
35
36 [
37 (right_x, right_z),
38 (left_x, left_z),
39 (back_x + right_x, back_z + right_z),
40 (back_x + left_x, back_z + left_z),
41 (forward_x + right_x, forward_z + right_z),
42 (forward_x + left_x, forward_z + left_z),
43 (back_x, back_z),
44 (forward_x, forward_z),
45 ]
46}
47
48#[must_use]
49pub(crate) fn can_dismount_to(world: &Arc<World>, aabb: &WorldAabb) -> bool {
50 let collision_world = WorldCollisionProvider::new(world);
51 !collision_world.has_block_collision_with_context(aabb, BlockCollisionContext::empty())
52 && world.world_border_snapshot().is_within_bounds(*aabb)
53}
54
55#[must_use]
56#[expect(
57 dead_code,
58 reason = "vanilla DismountHelper foundation; vehicle dismounts use this next"
59)]
60pub(crate) fn can_dismount_to_pose(
61 world: &Arc<World>,
62 location: DVec3,
63 passenger: &dyn Entity,
64 dismount_pose: EntityPose,
65) -> bool {
66 let dimensions = passenger.dimensions_for_pose(dismount_pose);
67 let aabb = WorldAabb::entity_box(
68 location.x,
69 location.y,
70 location.z,
71 f64::from(dimensions.half_width()),
72 f64::from(dimensions.height),
73 );
74 can_dismount_to(world, &aabb)
75}
76
77#[must_use]
78#[expect(
79 dead_code,
80 reason = "vanilla DismountHelper foundation; vehicle dismounts use this next"
81)]
82pub(crate) fn find_ceiling_from(
83 pos: BlockPos,
84 blocks: i32,
85 mut shape_getter: impl FnMut(BlockPos) -> OffsetVoxelShape,
86) -> f64 {
87 let mut y = 0;
88 while y < blocks {
89 let cursor = BlockPos::new(pos.x(), pos.y() + y, pos.z());
90 let collision_shape = shape_getter(cursor);
91 if !collision_shape.is_empty() {
92 return f64::from(pos.y() + y) + collision_shape.min(Axis::Y);
93 }
94 y += 1;
95 }
96
97 f64::INFINITY
98}
99
100#[must_use]
101pub(crate) fn find_safe_dismount_location(
102 world: &Arc<World>,
103 entity: &dyn Entity,
104 block_pos: BlockPos,
105 check_dangerous: bool,
106) -> Option<DVec3> {
107 if check_dangerous && is_block_dangerous(entity, world.get_block_state(block_pos)) {
108 return None;
109 }
110
111 let floor_height = block_floor_height(
112 non_climbable_shape(world, block_pos),
113 non_climbable_shape(world, block_pos.below()),
114 );
115 if !is_block_floor_valid(floor_height) {
116 return None;
117 }
118
119 if check_dangerous
120 && floor_height <= 0.0
121 && is_block_dangerous(entity, world.get_block_state(block_pos.below()))
122 {
123 return None;
124 }
125
126 let position = DVec3::new(
127 f64::from(block_pos.x()) + 0.5,
128 f64::from(block_pos.y()) + floor_height,
129 f64::from(block_pos.z()) + 0.5,
130 );
131 let dimensions = entity.dimensions_for_pose(EntityPose::Standing);
132 let aabb = WorldAabb::entity_box(
133 position.x,
134 position.y,
135 position.z,
136 f64::from(dimensions.half_width()),
137 f64::from(dimensions.height),
138 );
139 if !can_dismount_to(world, &aabb) {
140 return None;
141 }
142
143 if entity.entity_type() == &vanilla_entities::PLAYER
144 && (world
145 .get_block_state(block_pos)
146 .get_block()
147 .has_tag(&BlockTag::INVALID_SPAWN_INSIDE)
148 || world
149 .get_block_state(block_pos.above())
150 .get_block()
151 .has_tag(&BlockTag::INVALID_SPAWN_INSIDE))
152 {
153 return None;
154 }
155
156 Some(position)
157}
158
159fn non_climbable_shape(world: &Arc<World>, pos: BlockPos) -> OffsetVoxelShape {
160 let state = world.get_block_state(pos);
161 let block = state.get_block();
162 let is_open_trapdoor = block.has_tag(&BlockTag::TRAPDOORS)
163 && state.try_get_value(&BlockStateProperties::OPEN) == Some(true);
164
165 if block.has_tag(&BlockTag::CLIMBABLE) || is_open_trapdoor {
166 return OffsetVoxelShape::without_offset(VoxelShape::EMPTY);
167 }
168
169 let behavior = BLOCK_BEHAVIORS.get_behavior(block);
170 let shape =
171 behavior.get_collision_shape(state, world.as_ref(), pos, BlockCollisionContext::empty());
172 if shape.is_empty() {
173 return OffsetVoxelShape::without_offset(shape);
174 }
175
176 OffsetVoxelShape::new(
177 shape,
178 behavior.get_collision_shape_offset(
179 state,
180 world.as_ref(),
181 pos,
182 BlockCollisionContext::empty(),
183 ),
184 )
185}
186
187fn block_floor_height(block_shape: OffsetVoxelShape, below_block_shape: OffsetVoxelShape) -> f64 {
188 if !block_shape.is_empty() {
189 return block_shape.max(Axis::Y);
190 }
191
192 let below_floor = below_block_shape.max(Axis::Y);
193 if below_floor >= 1.0 {
194 below_floor - 1.0
195 } else {
196 f64::NEG_INFINITY
197 }
198}
199
200fn is_block_floor_valid(block_floor_height: f64) -> bool {
201 !block_floor_height.is_infinite() && block_floor_height < 1.0
202}
203
204fn is_block_dangerous(entity: &dyn Entity, state: BlockStateId) -> bool {
205 if !entity.fire_immune() && WalkPathEvaluator::is_burning_block(state) {
207 return true;
208 }
209
210 let block = state.get_block();
211 block == &vanilla_blocks::WITHER_ROSE
212 || block == &vanilla_blocks::SWEET_BERRY_BUSH
213 || block == &vanilla_blocks::CACTUS
214 || block == &vanilla_blocks::POWDER_SNOW
215}