Skip to main content

steel_core/portal/
end_gateway.rs

1//! End gateway destination calculation.
2
3use std::sync::Arc;
4
5use glam::DVec3;
6use steel_protocol::packets::game::RelativeMovement;
7use steel_registry::vanilla_entities;
8use steel_utils::{BlockPos, ChunkPos, Downcast as _, SectionPos};
9
10use crate::{
11    block_entity::entities::EndGatewayBlockEntity,
12    entity::Entity,
13    portal::{PortalTicketTarget, TeleportPostTransition, TeleportTransition},
14    world::World,
15};
16
17const GATEWAY_HEIGHT_ABOVE_SURFACE: i32 = 10;
18const EXIT_PORTAL_SEARCH_DISTANCE: f64 = 1024.0;
19const EXIT_PORTAL_SEARCH_STEP: f64 = 16.0;
20const EXIT_PORTAL_SEARCH_LIMIT: i32 = 16;
21const EXIT_POSITION_SEARCH_RADIUS: i32 = 5;
22const VALID_TELEPORT_SEARCH_RADIUS: i32 = 16;
23const GENERATED_ISLAND_Y: i32 = 75;
24
25/// Initial chunk preparation needed before resolving an End gateway transition.
26pub(crate) enum EndGatewayChunkPreparation {
27    /// Requested chunks are enough to calculate the transition after they load.
28    Ready(Vec<ChunkPos>),
29    /// Requested chunks only cover vanilla's tentative outer-island search.
30    SearchPath(Vec<ChunkPos>),
31}
32
33#[derive(Clone, Copy, Debug, PartialEq, Eq)]
34enum GatewayExitState {
35    Stored { exit: BlockPos, exact: bool },
36    Missing { exact: bool },
37}
38
39#[derive(Clone, Copy, Debug, PartialEq, Eq)]
40enum GatewayTeleportAnchor {
41    Existing(BlockPos),
42    NeedsIsland(BlockPos),
43}
44
45impl GatewayTeleportAnchor {
46    const fn pos(self) -> BlockPos {
47        match self {
48            Self::Existing(pos) | Self::NeedsIsland(pos) => pos,
49        }
50    }
51}
52
53/// Returns the first chunks that must be ready before an End gateway transition can be resolved.
54#[must_use]
55pub(crate) fn initial_chunks(
56    world: &World,
57    portal_pos: BlockPos,
58    source_is_end: bool,
59) -> Option<EndGatewayChunkPreparation> {
60    match gateway_exit_state(world, portal_pos)? {
61        GatewayExitState::Stored { exit, exact: true } => Some(EndGatewayChunkPreparation::Ready(
62            chunks_for_block_square(exit, 0),
63        )),
64        GatewayExitState::Stored { exit, exact: false } => Some(EndGatewayChunkPreparation::Ready(
65            chunks_for_block_square(exit.offset(0, 2, 0), EXIT_POSITION_SEARCH_RADIUS),
66        )),
67        GatewayExitState::Missing { .. } if source_is_end => Some(
68            EndGatewayChunkPreparation::SearchPath(exit_search_candidate_chunks(portal_pos)),
69        ),
70        GatewayExitState::Missing { .. } => None,
71    }
72}
73
74/// Returns final chunks needed after the tentative outer-island search chunks are ready.
75#[must_use]
76pub(crate) fn final_chunks_after_search(
77    world: &World,
78    portal_pos: BlockPos,
79    source_is_end: bool,
80) -> Option<Vec<ChunkPos>> {
81    match gateway_exit_state(world, portal_pos)? {
82        GatewayExitState::Stored { exit, exact: true } => Some(chunks_for_block_square(exit, 0)),
83        GatewayExitState::Stored { exit, exact: false } => Some(chunks_for_block_square(
84            exit.offset(0, 2, 0),
85            EXIT_POSITION_SEARCH_RADIUS,
86        )),
87        GatewayExitState::Missing { .. } if source_is_end => {
88            let anchor = find_teleport_anchor(world, portal_pos)?;
89            Some(chunks_for_block_square(
90                anchor.pos(),
91                VALID_TELEPORT_SEARCH_RADIUS,
92            ))
93        }
94        GatewayExitState::Missing { .. } => None,
95    }
96}
97
98/// Calculates vanilla's End gateway transition after the required chunks are available.
99#[must_use]
100pub(crate) fn calculate_transition(
101    world: &Arc<World>,
102    entity: &dyn Entity,
103    portal_pos: BlockPos,
104    source_is_end: bool,
105) -> Option<TeleportTransition> {
106    let (exit, exact) = match gateway_exit_state(world, portal_pos)? {
107        GatewayExitState::Stored { exit, exact } => (exit, exact),
108        GatewayExitState::Missing { exact } if source_is_end => {
109            let exit = find_or_create_valid_teleport_pos(world, portal_pos)?
110                .above_n(GATEWAY_HEIGHT_ABOVE_SURFACE);
111            if !world.create_end_gateway_portal(exit, portal_pos, false) {
112                log::error!("Unable to create End gateway portal at {}", world.key);
113                return None;
114            }
115            if !set_gateway_exit_position(world, portal_pos, exit, exact) {
116                return None;
117            }
118            (exit, exact)
119        }
120        GatewayExitState::Missing { .. } => return None,
121    };
122
123    let destination = if exact {
124        exit
125    } else {
126        find_exit_position(world, exit)
127    };
128    Some(gateway_transition(world, entity, destination))
129}
130
131fn gateway_exit_state(world: &World, portal_pos: BlockPos) -> Option<GatewayExitState> {
132    let block_entity = world.get_block_entity(portal_pos)?;
133    let gateway = block_entity.downcast_ref::<EndGatewayBlockEntity>()?;
134    Some(match gateway.exit_portal() {
135        Some(exit) => GatewayExitState::Stored {
136            exit,
137            exact: gateway.exact_teleport(),
138        },
139        None => GatewayExitState::Missing {
140            exact: gateway.exact_teleport(),
141        },
142    })
143}
144
145fn set_gateway_exit_position(
146    world: &World,
147    portal_pos: BlockPos,
148    exit: BlockPos,
149    exact: bool,
150) -> bool {
151    let Some(block_entity) = world.get_block_entity(portal_pos) else {
152        return false;
153    };
154    let Some(gateway) = block_entity.downcast_ref::<EndGatewayBlockEntity>() else {
155        return false;
156    };
157    gateway.set_exit_position(exit, exact);
158    true
159}
160
161fn find_exit_position(world: &World, exit_portal: BlockPos) -> BlockPos {
162    world
163        .find_end_gateway_tallest_block(
164            exit_portal.offset(0, 2, 0),
165            EXIT_POSITION_SEARCH_RADIUS,
166            false,
167        )
168        .above()
169}
170
171fn find_or_create_valid_teleport_pos(
172    world: &Arc<World>,
173    gateway_pos: BlockPos,
174) -> Option<BlockPos> {
175    let anchor = find_teleport_anchor(world, gateway_pos)?;
176    if let GatewayTeleportAnchor::NeedsIsland(pos) = anchor
177        && !world.create_end_island(pos)
178    {
179        log::error!("Unable to create End island at {}", world.key);
180        return None;
181    }
182
183    Some(world.find_end_gateway_tallest_block(anchor.pos(), VALID_TELEPORT_SEARCH_RADIUS, true))
184}
185
186fn find_teleport_anchor(world: &World, gateway_pos: BlockPos) -> Option<GatewayTeleportAnchor> {
187    let tentative = find_exit_portal_xz_pos_tentative(world, gateway_pos)?;
188    let chunk = chunk_for_xz_vec(tentative);
189    if let Some(pos) = world.find_end_gateway_valid_spawn_in_chunk(chunk) {
190        return Some(GatewayTeleportAnchor::Existing(pos));
191    }
192
193    Some(GatewayTeleportAnchor::NeedsIsland(BlockPos::new(
194        (tentative.x + 0.5).floor() as i32,
195        GENERATED_ISLAND_Y,
196        (tentative.z + 0.5).floor() as i32,
197    )))
198}
199
200fn find_exit_portal_xz_pos_tentative(world: &World, gateway_pos: BlockPos) -> Option<DVec3> {
201    let direction = xz_direction(gateway_pos);
202    let mut tentative = direction * EXIT_PORTAL_SEARCH_DISTANCE;
203
204    let mut remaining = EXIT_PORTAL_SEARCH_LIMIT;
205    while !is_chunk_empty(world, tentative)? && remaining > 0 {
206        remaining -= 1;
207        tentative -= direction * EXIT_PORTAL_SEARCH_STEP;
208    }
209
210    let mut remaining = EXIT_PORTAL_SEARCH_LIMIT;
211    while is_chunk_empty(world, tentative)? && remaining > 0 {
212        remaining -= 1;
213        tentative += direction * EXIT_PORTAL_SEARCH_STEP;
214    }
215
216    Some(tentative)
217}
218
219fn is_chunk_empty(world: &World, xz_pos: DVec3) -> Option<bool> {
220    world.is_end_gateway_chunk_empty(chunk_for_xz_vec(xz_pos))
221}
222
223fn gateway_transition(
224    world: &Arc<World>,
225    entity: &dyn Entity,
226    destination: BlockPos,
227) -> TeleportTransition {
228    let is_ender_pearl = entity.entity_type() == &vanilla_entities::ENDER_PEARL;
229    TeleportTransition {
230        target_world: world.clone(),
231        position: block_bottom_center(destination),
232        rotation: (0.0, 0.0),
233        velocity: DVec3::ZERO,
234        relatives: if is_ender_pearl {
235            RelativeMovement::NONE
236        } else {
237            RelativeMovement::DELTA.union(RelativeMovement::ROTATION)
238        },
239        portal_cooldown: entity.dimension_changing_delay(),
240        as_passenger: false,
241        post_transition: TeleportPostTransition::place_portal_ticket(
242            PortalTicketTarget::Destination,
243        ),
244    }
245}
246
247fn exit_search_candidate_chunks(gateway_pos: BlockPos) -> Vec<ChunkPos> {
248    let direction = xz_direction(gateway_pos);
249    let start = direction * EXIT_PORTAL_SEARCH_DISTANCE;
250    let mut chunks = Vec::with_capacity((EXIT_PORTAL_SEARCH_LIMIT * 2 + 1) as usize);
251    for step in -EXIT_PORTAL_SEARCH_LIMIT..=EXIT_PORTAL_SEARCH_LIMIT {
252        chunks.push(chunk_for_xz_vec(
253            start + direction * (f64::from(step) * EXIT_PORTAL_SEARCH_STEP),
254        ));
255    }
256    chunks
257}
258
259fn chunks_for_block_square(center: BlockPos, block_radius: i32) -> Vec<ChunkPos> {
260    let min_chunk_x = SectionPos::block_to_section_coord(center.x() - block_radius);
261    let max_chunk_x = SectionPos::block_to_section_coord(center.x() + block_radius);
262    let min_chunk_z = SectionPos::block_to_section_coord(center.z() - block_radius);
263    let max_chunk_z = SectionPos::block_to_section_coord(center.z() + block_radius);
264    let mut chunks = Vec::with_capacity(
265        ((max_chunk_x - min_chunk_x + 1) * (max_chunk_z - min_chunk_z + 1)) as usize,
266    );
267
268    for chunk_z in min_chunk_z..=max_chunk_z {
269        for chunk_x in min_chunk_x..=max_chunk_x {
270            chunks.push(ChunkPos::new(chunk_x, chunk_z));
271        }
272    }
273    chunks
274}
275
276fn chunk_for_xz_vec(pos: DVec3) -> ChunkPos {
277    ChunkPos::new((pos.x / 16.0).floor() as i32, (pos.z / 16.0).floor() as i32)
278}
279
280fn xz_direction(pos: BlockPos) -> DVec3 {
281    let vector = DVec3::new(f64::from(pos.x()), 0.0, f64::from(pos.z()));
282    let length = vector.length();
283    if length < 1.0E-4 {
284        DVec3::ZERO
285    } else {
286        vector / length
287    }
288}
289
290fn block_bottom_center(pos: BlockPos) -> DVec3 {
291    let (x, y, z) = pos.get_bottom_center();
292    DVec3::new(x, y, z)
293}
294
295#[cfg(test)]
296mod tests {
297    use super::{
298        EXIT_PORTAL_SEARCH_LIMIT, chunks_for_block_square, exit_search_candidate_chunks,
299        xz_direction,
300    };
301    use glam::DVec3;
302    use steel_utils::{BlockPos, ChunkPos};
303
304    #[test]
305    fn zero_gateway_position_has_zero_search_direction() {
306        assert_eq!(xz_direction(BlockPos::ZERO), DVec3::ZERO);
307    }
308
309    #[test]
310    fn exit_search_candidates_cover_vanilla_probe_range() {
311        let chunks = exit_search_candidate_chunks(BlockPos::new(1, 70, 0));
312
313        assert_eq!(chunks.len(), (EXIT_PORTAL_SEARCH_LIMIT * 2 + 1) as usize);
314        assert!(chunks.contains(&ChunkPos::new(48, 0)));
315        assert!(chunks.contains(&ChunkPos::new(64, 0)));
316        assert!(chunks.contains(&ChunkPos::new(80, 0)));
317    }
318
319    #[test]
320    fn block_square_chunks_cover_radius_across_chunk_edges() {
321        let chunks = chunks_for_block_square(BlockPos::new(16, 70, 16), 5);
322
323        assert_eq!(
324            chunks,
325            vec![
326                ChunkPos::new(0, 0),
327                ChunkPos::new(1, 0),
328                ChunkPos::new(0, 1),
329                ChunkPos::new(1, 1),
330            ]
331        );
332    }
333}