Skip to main content

steel_core/entity/
dismount_helper.rs

1//! Vanilla dismount location helper
2
3use 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    // TODO: mirror vanilla entitytype.immuneto when the entity types carry entity specific immune block tag
206    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}