1use 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
25pub(crate) enum EndGatewayChunkPreparation {
27 Ready(Vec<ChunkPos>),
29 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#[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#[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#[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}