1use super::*;
2
3pub(super) fn portal_candidate_distance_sqr(candidate: BlockPos, center: BlockPos) -> i64 {
4 let dx = i64::from(candidate.x()) - i64::from(center.x());
5 let dy = i64::from(candidate.y()) - i64::from(center.y());
6 let dz = i64::from(candidate.z()) - i64::from(center.z());
7 dx * dx + dy * dy + dz * dz
8}
9
10pub(super) fn dist_to_origin_center_sqr(pos: BlockPos) -> f64 {
11 let x = f64::from(pos.x()) + 0.5;
12 let y = f64::from(pos.y()) + 0.5;
13 let z = f64::from(pos.z()) + 0.5;
14 x * x + y * y + z * z
15}
16
17pub(super) fn closest_portal_candidate(
18 candidates: impl IntoIterator<Item = BlockPos>,
19 approximate_exit_pos: BlockPos,
20 is_valid: impl Fn(BlockPos) -> bool,
21) -> Option<BlockPos> {
22 candidates
23 .into_iter()
24 .filter(|pos| is_valid(*pos))
25 .min_by_key(|pos| {
26 (
27 portal_candidate_distance_sqr(*pos, approximate_exit_pos),
28 pos.y(),
29 )
30 })
31}
32
33const NETHER_PORTAL_CREATE_RADIUS: i32 = 16;
34const NETHER_PORTAL_FALLBACK_MIN_Y: i32 = 70;
35const NETHER_PORTAL_FALLBACK_MAX_Y_OFFSET: i32 = 9;
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub(super) struct MissingPortalCreationChunk;
39
40pub(super) const fn nether_portal_frame_offset_pos(
41 origin: BlockPos,
42 direction: Direction,
43 width: i32,
44 height: i32,
45 offset: i32,
46) -> BlockPos {
47 let clockwise = direction.rotate_y_clockwise();
48 let (direction_x, _, direction_z) = direction.offset();
49 let (clockwise_x, _, clockwise_z) = clockwise.offset();
50 origin.offset(
51 direction_x * width + clockwise_x * offset,
52 height,
53 direction_z * width + clockwise_z * offset,
54 )
55}
56
57pub(super) fn nether_portal_creation_scan_origin(
58 column_pos: BlockPos,
59 direction: Direction,
60 height: i32,
61) -> BlockPos {
62 column_pos.relative(direction.opposite()).at_y(height)
63}
64
65impl World {
66 #[must_use]
74 pub fn find_closest_nether_portal_position(
75 &self,
76 approximate_exit_pos: BlockPos,
77 to_nether: bool,
78 ) -> Option<BlockPos> {
79 let radius = if to_nether { 16 } else { 128 };
80 let nether_portal_type = vanilla_poi_types::NETHER_PORTAL
81 .try_id()
82 .expect("vanilla nether portal POI type should be registered");
83 let candidates = self.poi_storage.lock().get_in_horizontal_square(
84 &|type_id| type_id == nether_portal_type,
85 approximate_exit_pos,
86 radius,
87 OccupationStatus::Any,
88 );
89
90 closest_portal_candidate(
91 candidates.into_iter().map(|(pos, _)| pos),
92 approximate_exit_pos,
93 |pos| {
94 self.is_block_within_world_border(pos)
95 && self
96 .get_block_state(pos)
97 .try_get_value(&BlockStateProperties::HORIZONTAL_AXIS)
98 .is_some()
99 },
100 )
101 }
102
103 #[must_use]
109 pub fn create_nether_portal(
110 self: &Arc<Self>,
111 origin: BlockPos,
112 portal_axis: Axis,
113 ) -> Option<FoundRectangle> {
114 if portal_axis == Axis::Y {
115 return None;
116 }
117
118 let direction = Direction::positive_for_axis(portal_axis);
119 let max_placeable_y = self
120 .get_max_y()
121 .min(self.get_min_y() + self.dimension_type.logical_height - 1);
122
123 let portal_origin =
124 match self.find_nether_portal_creation_position(origin, direction, max_placeable_y) {
125 Ok(Some(pos)) => pos,
126 Ok(None) => {
127 let fallback =
128 self.fallback_nether_portal_position(origin, direction, max_placeable_y)?;
129 if !self.can_write_nether_portal_fallback_box(fallback, direction) {
130 return None;
131 }
132 if !self.clear_nether_portal_fallback_box(fallback, direction) {
133 return None;
134 }
135 fallback
136 }
137 Err(MissingPortalCreationChunk) => return None,
138 };
139
140 if !self.can_write_nether_portal_rectangle(portal_origin, direction) {
141 return None;
142 }
143 if !self.place_nether_portal_frame_and_blocks(portal_origin, direction, portal_axis) {
144 return None;
145 }
146
147 Some(FoundRectangle {
148 min_corner: portal_origin,
149 axis1_size: 2,
150 axis2_size: 3,
151 })
152 }
153
154 pub(crate) fn place_portal_ticket(&self, ticket_position: BlockPos) {
156 self.chunk_map.place_portal_ticket(ticket_position);
157 }
158
159 pub(super) fn find_nether_portal_creation_position(
160 &self,
161 origin: BlockPos,
162 direction: Direction,
163 max_placeable_y: i32,
164 ) -> Result<Option<BlockPos>, MissingPortalCreationChunk> {
165 let mut closest_full_position: Option<(i64, BlockPos)> = None;
166 let mut closest_partial_position: Option<(i64, BlockPos)> = None;
167 let border = self.world_border_snapshot();
168
169 for column_pos in BlockPos::spiral_around(
170 origin,
171 NETHER_PORTAL_CREATE_RADIUS,
172 Direction::East,
173 Direction::South,
174 ) {
175 let height = self
176 .height_at(
177 HeightmapType::MotionBlocking,
178 column_pos.x(),
179 column_pos.z(),
180 )
181 .ok_or(MissingPortalCreationChunk)?
182 .min(max_placeable_y);
183 if !border.is_block_within_bounds(column_pos)
184 || !border.is_block_within_bounds(column_pos.relative(direction))
185 {
186 continue;
187 }
188
189 let mut column_pos = nether_portal_creation_scan_origin(column_pos, direction, height);
190 let mut y = height;
191 while y >= self.get_min_y() {
192 column_pos = column_pos.at_y(y);
193 if self.can_nether_portal_replace_block(column_pos)? {
194 let first_empty_y = y;
195
196 while y > self.get_min_y()
197 && self.can_nether_portal_replace_block(column_pos.below())?
198 {
199 y -= 1;
200 column_pos = column_pos.below();
201 }
202
203 if y + 4 <= max_placeable_y {
204 let delta_y = first_empty_y - y;
205 if (delta_y <= 0 || delta_y >= 3)
206 && self.can_host_nether_portal_frame(column_pos, direction, 0)?
207 {
208 let distance = portal_candidate_distance_sqr(column_pos, origin);
209 let full_frame = self
210 .can_host_nether_portal_frame(column_pos, direction, -1)?
211 && self.can_host_nether_portal_frame(column_pos, direction, 1)?;
212
213 if full_frame
214 && closest_full_position
215 .is_none_or(|(closest_distance, _)| closest_distance > distance)
216 {
217 closest_full_position = Some((distance, column_pos));
218 }
219
220 if closest_full_position.is_none()
221 && closest_partial_position
222 .is_none_or(|(closest_distance, _)| closest_distance > distance)
223 {
224 closest_partial_position = Some((distance, column_pos));
225 }
226 }
227 }
228 }
229
230 y -= 1;
231 }
232 }
233
234 if closest_full_position.is_none() {
235 closest_full_position = closest_partial_position;
236 }
237
238 Ok(closest_full_position.map(|(_, pos)| pos))
239 }
240
241 pub(super) fn can_nether_portal_replace_block(
242 &self,
243 pos: BlockPos,
244 ) -> Result<bool, MissingPortalCreationChunk> {
245 let state = self
246 .loaded_block_state(pos)
247 .ok_or(MissingPortalCreationChunk)?;
248 Ok(state.is_replaceable() && state.get_fluid_state().is_empty())
249 }
250
251 pub(super) fn can_host_nether_portal_frame(
252 &self,
253 origin: BlockPos,
254 direction: Direction,
255 offset: i32,
256 ) -> Result<bool, MissingPortalCreationChunk> {
257 for width in -1..3 {
258 for height in -1..4 {
259 let pos = nether_portal_frame_offset_pos(origin, direction, width, height, offset);
260 if height < 0 {
261 let state = self
262 .loaded_block_state(pos)
263 .ok_or(MissingPortalCreationChunk)?;
264 if !state.is_solid() {
265 return Ok(false);
266 }
267 } else if !self.can_nether_portal_replace_block(pos)? {
268 return Ok(false);
269 }
270 }
271 }
272
273 Ok(true)
274 }
275
276 pub(super) fn loaded_block_state(&self, pos: BlockPos) -> Option<BlockStateId> {
277 if !self.is_in_valid_bounds(pos) {
278 return Some(REGISTRY.blocks.get_base_state_id(&vanilla_blocks::AIR));
279 }
280
281 let chunk_pos = Self::chunk_pos_for_block(pos);
282 self.chunk_map
283 .with_full_chunk(chunk_pos, |chunk| chunk.get_block_state(pos))
284 }
285
286 pub(super) fn fallback_nether_portal_position(
287 &self,
288 origin: BlockPos,
289 direction: Direction,
290 max_placeable_y: i32,
291 ) -> Option<BlockPos> {
292 let min_start_y = (self.get_min_y() + 1).max(NETHER_PORTAL_FALLBACK_MIN_Y);
293 let max_start_y = max_placeable_y - NETHER_PORTAL_FALLBACK_MAX_Y_OFFSET;
294 if max_start_y < min_start_y {
295 return None;
296 }
297
298 let (direction_x, _, direction_z) = direction.offset();
299 let pos = BlockPos::new(
300 origin.x() - direction_x,
301 origin.y().clamp(min_start_y, max_start_y),
302 origin.z() - direction_z,
303 );
304
305 Some(self.world_border_snapshot().clamp_to_bounds(
306 f64::from(pos.x()),
307 f64::from(pos.y()),
308 f64::from(pos.z()),
309 ))
310 }
311
312 pub(super) fn can_write_nether_portal_fallback_box(
313 &self,
314 origin: BlockPos,
315 direction: Direction,
316 ) -> bool {
317 for box_offset in -1..2 {
318 for width in 0..2 {
319 for height in -1..3 {
320 let pos = nether_portal_frame_offset_pos(
321 origin, direction, width, height, box_offset,
322 );
323 if !self.can_write_loaded_block(pos) {
324 return false;
325 }
326 }
327 }
328 }
329
330 self.can_write_nether_portal_rectangle(origin, direction)
331 }
332
333 pub(super) fn can_write_nether_portal_rectangle(
334 &self,
335 origin: BlockPos,
336 direction: Direction,
337 ) -> bool {
338 for width in -1..3 {
339 for height in -1..4 {
340 let pos = nether_portal_frame_offset_pos(origin, direction, width, height, 0);
341 if !self.can_write_loaded_block(pos) {
342 return false;
343 }
344 }
345 }
346
347 true
348 }
349
350 pub(super) fn can_write_loaded_block(&self, pos: BlockPos) -> bool {
351 if !self.is_in_valid_bounds(pos) {
352 return false;
353 }
354
355 let chunk_pos = Self::chunk_pos_for_block(pos);
356 self.chunk_map.with_full_chunk(chunk_pos, |_| ()).is_some()
357 }
358
359 pub(crate) fn create_end_platform(self: &Arc<Self>, origin: BlockPos) -> bool {
361 let obsidian = vanilla_blocks::OBSIDIAN.default_state();
362 let air = vanilla_blocks::AIR.default_state();
363
364 for dz in -2..=2 {
365 for dx in -2..=2 {
366 for dy in -1..3 {
367 let pos = origin.offset(dx, dy, dz);
368 let state = if dy == -1 { obsidian } else { air };
369 if self.get_block_state(pos).get_block() != state.get_block() {
370 let _ = self.destroy_block(pos, true);
371 if !self.set_block(pos, state, UpdateFlags::UPDATE_ALL) {
372 return false;
373 }
374 }
375 }
376 }
377 }
378
379 true
380 }
381
382 pub(crate) fn is_end_gateway_chunk_empty(&self, chunk_pos: ChunkPos) -> Option<bool> {
383 self.chunk_map.with_full_chunk(chunk_pos, |chunk| {
384 chunk.highest_filled_section_index().is_none()
385 })
386 }
387
388 pub(crate) fn find_end_gateway_valid_spawn_in_chunk(
389 &self,
390 chunk_pos: ChunkPos,
391 ) -> Option<BlockPos> {
392 self.chunk_map
393 .with_full_chunk(chunk_pos, |chunk| {
394 let min_x = chunk_pos.0.x * 16;
395 let min_z = chunk_pos.0.y * 16;
396 let max_x = min_x + 15;
397 let max_z = min_z + 15;
398 let max_y = chunk.highest_section_position() + 16 - 1;
399 let min_y = 30.min(max_y);
400 let max_y = 30.max(max_y);
401 let mut closest = None;
402 let mut closest_dist = 0.0;
403
404 for z in min_z..=max_z {
405 for y in min_y..=max_y {
406 for x in min_x..=max_x {
407 let pos = BlockPos::new(x, y, z);
408 let state = chunk.get_block_state(pos);
409 let above = pos.above();
410 let above_two = pos.above_n(2);
411 if state.get_block() != &vanilla_blocks::END_STONE
412 || self.is_collision_shape_full_block_at(
413 above,
414 chunk.get_block_state(above),
415 )
416 || self.is_collision_shape_full_block_at(
417 above_two,
418 chunk.get_block_state(above_two),
419 )
420 {
421 continue;
422 }
423
424 let dist = dist_to_origin_center_sqr(pos);
425 if closest.is_none() || dist < closest_dist {
426 closest = Some(pos);
427 closest_dist = dist;
428 }
429 }
430 }
431 }
432
433 closest
434 })
435 .flatten()
436 }
437
438 pub(crate) fn find_end_gateway_tallest_block(
439 &self,
440 around: BlockPos,
441 dist: i32,
442 allow_bedrock: bool,
443 ) -> BlockPos {
444 let mut tallest = None;
445
446 for dx in -dist..=dist {
447 for dz in -dist..=dist {
448 if dx == 0 && dz == 0 && !allow_bedrock {
449 continue;
450 }
451
452 let min_y = tallest.map_or(self.get_min_y(), |pos: BlockPos| pos.y());
453 for y in (min_y + 1..=self.get_max_y()).rev() {
454 let pos = BlockPos::new(around.x() + dx, y, around.z() + dz);
455 let state = self.get_block_state(pos);
456 if self.is_collision_shape_full_block_at(pos, state)
457 && (allow_bedrock || state.get_block() != &vanilla_blocks::BEDROCK)
458 {
459 tallest = Some(pos);
460 break;
461 }
462 }
463 }
464 }
465
466 tallest.unwrap_or(around)
467 }
468
469 pub(super) fn is_collision_shape_full_block_at(
470 &self,
471 pos: BlockPos,
472 state: BlockStateId,
473 ) -> bool {
474 is_shape_full_block(self.block_collision_shape(pos, state))
475 }
476
477 pub(crate) fn create_end_island(self: &Arc<Self>, origin: BlockPos) -> bool {
479 let end_stone = vanilla_blocks::END_STONE.default_state();
480 let mut random = LegacyRandom::from_seed(PackedBlockPos::from(origin).as_raw() as u64);
481 let mut size = random.next_i32_bounded(3) as f32 + 4.0;
482 let mut y = 0;
483
484 while size > 0.5 {
485 let min = (-size).floor() as i32;
486 let max = size.ceil() as i32;
487 for x in min..=max {
488 for z in min..=max {
489 if (x * x + z * z) as f32 <= (size + 1.0) * (size + 1.0)
490 && !self.set_block(
491 origin.offset(x, y, z),
492 end_stone,
493 UpdateFlags::UPDATE_CLIENTS,
494 )
495 {
496 return false;
497 }
498 }
499 }
500
501 size -= random.next_i32_bounded(2) as f32 + 0.5;
502 y -= 1;
503 }
504
505 true
506 }
507
508 pub(crate) fn create_end_gateway_portal(
510 self: &Arc<Self>,
511 origin: BlockPos,
512 exit: BlockPos,
513 exact: bool,
514 ) -> bool {
515 for dy in -2_i32..=2 {
516 for dx in -1..=1 {
517 for dz in -1..=1 {
518 let same_x = dx == 0;
519 let same_y = dy == 0;
520 let same_z = dz == 0;
521 let end = dy.abs() == 2;
522 let state = if same_x && same_y && same_z {
523 vanilla_blocks::END_GATEWAY.default_state()
524 } else if same_y {
525 vanilla_blocks::AIR.default_state()
526 } else if (end && same_x && same_z) || ((same_x || same_z) && !end) {
527 vanilla_blocks::BEDROCK.default_state()
528 } else {
529 vanilla_blocks::AIR.default_state()
530 };
531
532 if !self.set_block(origin.offset(dx, dy, dz), state, UpdateFlags::UPDATE_ALL) {
533 return false;
534 }
535 }
536 }
537 }
538
539 let Some(block_entity) = self.get_block_entity(origin) else {
540 return false;
541 };
542 let Some(gateway) = block_entity.downcast_ref::<EndGatewayBlockEntity>() else {
543 return false;
544 };
545 gateway.set_exit_position(exit, exact);
546 true
547 }
548
549 pub(super) fn clear_nether_portal_fallback_box(
550 self: &Arc<Self>,
551 origin: BlockPos,
552 direction: Direction,
553 ) -> bool {
554 let obsidian = vanilla_blocks::OBSIDIAN.default_state();
555 let air = vanilla_blocks::AIR.default_state();
556
557 for box_offset in -1..2 {
558 for width in 0..2 {
559 for height in -1..3 {
560 let state = if height < 0 { obsidian } else { air };
561 let pos = nether_portal_frame_offset_pos(
562 origin, direction, width, height, box_offset,
563 );
564 if !self.set_block(pos, state, UpdateFlags::UPDATE_ALL) {
565 return false;
566 }
567 }
568 }
569 }
570
571 true
572 }
573
574 pub(super) fn place_nether_portal_frame_and_blocks(
575 self: &Arc<Self>,
576 origin: BlockPos,
577 direction: Direction,
578 portal_axis: Axis,
579 ) -> bool {
580 let obsidian = vanilla_blocks::OBSIDIAN.default_state();
581 for width in -1..3 {
582 for height in -1..4 {
583 if width == -1 || width == 2 || height == -1 || height == 3 {
584 let pos = nether_portal_frame_offset_pos(origin, direction, width, height, 0);
585 if !self.set_block(pos, obsidian, UpdateFlags::UPDATE_ALL) {
586 return false;
587 }
588 }
589 }
590 }
591
592 let portal_state = vanilla_blocks::NETHER_PORTAL
593 .default_state()
594 .set_value(&BlockStateProperties::HORIZONTAL_AXIS, portal_axis);
595 let portal_flags = UpdateFlags::UPDATE_CLIENTS | UpdateFlags::UPDATE_KNOWN_SHAPE;
596 for width in 0..2 {
597 for height in 0..3 {
598 let pos = nether_portal_frame_offset_pos(origin, direction, width, height, 0);
599 if !self.set_block(pos, portal_state, portal_flags) {
600 return false;
601 }
602 }
603 }
604
605 true
606 }
607}