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