Skip to main content

steel_core/portal/
nether_portal.rs

1//! Nether portal destination calculation.
2
3use std::sync::Arc;
4
5use glam::DVec3;
6use steel_math::DEGREE_90;
7use steel_protocol::packets::game::RelativeMovement;
8use steel_registry::{
9    blocks::{block_state_ext::BlockStateExt, properties::BlockStateProperties},
10    dimension_type::DimensionType,
11};
12use steel_utils::{
13    BlockPos, ChunkPos, SectionPos,
14    axis::Axis,
15    block_util::{FoundRectangle, get_largest_rectangle_around},
16};
17
18use crate::{
19    entity::Entity,
20    portal::{
21        PortalTicketTarget, TeleportPostTransition, TeleportTransition, portal_shape::PortalShape,
22    },
23    world::World,
24};
25
26const NETHER_TARGET_PORTAL_SEARCH_RADIUS: i32 = 16;
27const OVERWORLD_TARGET_PORTAL_SEARCH_RADIUS: i32 = 128;
28const PORTAL_RECTANGLE_SCAN_LIMIT: i32 = 21;
29
30/// Returns vanilla's target portal search radius for Nether portal dimension changes.
31#[must_use]
32pub(crate) const fn search_radius(to_nether: bool) -> i32 {
33    if to_nether {
34        NETHER_TARGET_PORTAL_SEARCH_RADIUS
35    } else {
36        OVERWORLD_TARGET_PORTAL_SEARCH_RADIUS
37    }
38}
39
40/// Returns the full-chunk square radius Steel prewarms before resolving a Nether portal exit.
41#[must_use]
42pub(crate) const fn prewarm_chunk_radius(to_nether: bool) -> u8 {
43    let chunk_radius = search_radius(to_nether) / 16 + 2;
44    chunk_radius as u8
45}
46
47/// Returns the chunk centered on a block position for portal prewarming.
48#[must_use]
49pub(crate) const fn prewarm_center(pos: BlockPos) -> ChunkPos {
50    ChunkPos::new(
51        SectionPos::block_to_section_coord(pos.x()),
52        SectionPos::block_to_section_coord(pos.z()),
53    )
54}
55
56/// Returns vanilla's scaled and world-border-clamped approximate exit position.
57#[must_use]
58pub(crate) fn approximate_exit_position(
59    source_world: &World,
60    target_world: &World,
61    entity_position: DVec3,
62) -> BlockPos {
63    let scale = DimensionType::get_teleportation_scale(
64        source_world.dimension_type,
65        target_world.dimension_type,
66    );
67    target_world.clamp_to_world_border(
68        entity_position.x * scale,
69        entity_position.y,
70        entity_position.z * scale,
71    )
72}
73
74/// Calculates a Nether portal teleport transition after the target chunks are available.
75#[must_use]
76pub(crate) fn calculate_transition(
77    source_world: &Arc<World>,
78    target_world: &Arc<World>,
79    entity: &dyn Entity,
80    portal_entry_pos: BlockPos,
81    approximate_exit_pos: BlockPos,
82    to_nether: bool,
83) -> Option<TeleportTransition> {
84    let exit_portal_pos =
85        target_world.find_closest_nether_portal_position(approximate_exit_pos, to_nether);
86    let (exit_portal, ticket_target) = if let Some(pos) = exit_portal_pos {
87        (
88            largest_portal_rectangle_at(target_world, pos)?,
89            PortalTicketTarget::Block(pos),
90        )
91    } else {
92        let source_portal_axis = source_world
93            .get_block_state(portal_entry_pos)
94            .try_get_value(&BlockStateProperties::HORIZONTAL_AXIS)
95            .unwrap_or(Axis::X);
96        let Some(created) =
97            target_world.create_nether_portal(approximate_exit_pos, source_portal_axis)
98        else {
99            log::error!("Unable to create a portal, likely target out of world border");
100            return None;
101        };
102        (created, PortalTicketTarget::Destination)
103    };
104    let post_transition = TeleportPostTransition::play_portal_sound()
105        .then(TeleportPostTransition::place_portal_ticket(ticket_target));
106
107    Some(dimension_transition_from_exit(
108        source_world,
109        target_world,
110        entity,
111        portal_entry_pos,
112        exit_portal,
113        post_transition,
114    ))
115}
116
117fn largest_portal_rectangle_at(world: &World, pos: BlockPos) -> Option<FoundRectangle> {
118    let portal_state = world.get_block_state(pos);
119    let axis = portal_state.try_get_value(&BlockStateProperties::HORIZONTAL_AXIS)?;
120    Some(get_largest_rectangle_around(
121        pos,
122        axis,
123        PORTAL_RECTANGLE_SCAN_LIMIT,
124        Axis::Y,
125        PORTAL_RECTANGLE_SCAN_LIMIT,
126        |block_pos| world.get_block_state(block_pos) == portal_state,
127    ))
128}
129
130fn dimension_transition_from_exit(
131    source_world: &World,
132    target_world: &Arc<World>,
133    entity: &dyn Entity,
134    portal_entry_pos: BlockPos,
135    exit_portal: FoundRectangle,
136    post_transition: TeleportPostTransition,
137) -> TeleportTransition {
138    let source_portal_state = source_world.get_block_state(portal_entry_pos);
139    let (source_axis, offset) = if let Some(axis) =
140        source_portal_state.try_get_value(&BlockStateProperties::HORIZONTAL_AXIS)
141    {
142        let portal_area = get_largest_rectangle_around(
143            portal_entry_pos,
144            axis,
145            PORTAL_RECTANGLE_SCAN_LIMIT,
146            Axis::Y,
147            PORTAL_RECTANGLE_SCAN_LIMIT,
148            |pos| source_world.get_block_state(pos) == source_portal_state,
149        );
150        (axis, entity.get_relative_portal_position(axis, portal_area))
151    } else {
152        (Axis::X, DVec3::new(0.5, 0.0, 0.0))
153    };
154
155    create_dimension_transition(
156        target_world,
157        exit_portal,
158        source_axis,
159        offset,
160        entity,
161        post_transition,
162    )
163}
164
165fn create_dimension_transition(
166    target_world: &Arc<World>,
167    found_rectangle: FoundRectangle,
168    portal_axis: Axis,
169    offset: DVec3,
170    entity: &dyn Entity,
171    post_transition: TeleportPostTransition,
172) -> TeleportTransition {
173    let bottom_left = found_rectangle.min_corner;
174    let target_axis = target_world
175        .get_block_state(bottom_left)
176        .try_get_value(&BlockStateProperties::HORIZONTAL_AXIS)
177        .unwrap_or(Axis::X);
178    let width = f64::from(found_rectangle.axis1_size);
179    let height = f64::from(found_rectangle.axis2_size);
180    let dimensions = entity.dimensions_for_pose(entity.pose());
181    let entity_width = f64::from(dimensions.width);
182    let entity_height = f64::from(dimensions.height);
183    let output_rotation = if portal_axis == target_axis {
184        0.0
185    } else {
186        DEGREE_90
187    };
188    let offset_right = entity_width / 2.0 + (width - entity_width) * offset.x;
189    let offset_up = (height - entity_height) * offset.y;
190    let offset_forward = 0.5 + offset.z;
191    let x_aligned = target_axis == Axis::X;
192    let target_pos = DVec3::new(
193        f64::from(bottom_left.x())
194            + if x_aligned {
195                offset_right
196            } else {
197                offset_forward
198            },
199        f64::from(bottom_left.y()) + offset_up,
200        f64::from(bottom_left.z())
201            + if x_aligned {
202                offset_forward
203            } else {
204                offset_right
205            },
206    );
207    let collision_free_pos =
208        PortalShape::find_collision_free_position(target_pos, target_world, entity, dimensions);
209
210    TeleportTransition {
211        target_world: target_world.clone(),
212        position: collision_free_pos,
213        rotation: (output_rotation, 0.0),
214        velocity: DVec3::ZERO,
215        relatives: RelativeMovement::DELTA.union(RelativeMovement::ROTATION),
216        portal_cooldown: entity.dimension_changing_delay(),
217        as_passenger: false,
218        post_transition,
219    }
220}
221
222#[cfg(test)]
223mod tests {
224    use super::{prewarm_chunk_radius, search_radius};
225
226    #[test]
227    fn search_radius_matches_vanilla_portal_forcer_targets() {
228        assert_eq!(search_radius(true), 16);
229        assert_eq!(search_radius(false), 128);
230    }
231
232    #[test]
233    fn prewarm_radius_covers_poi_search_and_exit_rectangle_edges() {
234        assert_eq!(prewarm_chunk_radius(true), 3);
235        assert_eq!(prewarm_chunk_radius(false), 10);
236    }
237}