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> {
384 self.chunk_map.with_full_chunk(chunk_pos, |chunk| {
385 chunk.highest_filled_section_index().is_none()
386 })
387 }
388
389 pub(crate) fn find_end_gateway_valid_spawn_in_chunk(
391 &self,
392 chunk_pos: ChunkPos,
393 ) -> Option<BlockPos> {
394 self.chunk_map
395 .with_full_chunk(chunk_pos, |chunk| {
396 let min_x = chunk_pos.0.x * 16;
397 let min_z = chunk_pos.0.y * 16;
398 let max_x = min_x + 15;
399 let max_z = min_z + 15;
400 let max_y = chunk.highest_section_position() + 16 - 1;
401 let min_y = 30.min(max_y);
402 let max_y = 30.max(max_y);
403 let mut closest = None;
404 let mut closest_dist = 0.0;
405
406 for z in min_z..=max_z {
407 for y in min_y..=max_y {
408 for x in min_x..=max_x {
409 let pos = BlockPos::new(x, y, z);
410 let state = chunk.get_block_state(pos);
411 let above = pos.above();
412 let above_two = pos.above_n(2);
413 if state.get_block() != &vanilla_blocks::END_STONE
414 || self.is_collision_shape_full_block_at(
415 above,
416 chunk.get_block_state(above),
417 )
418 || self.is_collision_shape_full_block_at(
419 above_two,
420 chunk.get_block_state(above_two),
421 )
422 {
423 continue;
424 }
425
426 let dist = dist_to_origin_center_sqr(pos);
427 if closest.is_none() || dist < closest_dist {
428 closest = Some(pos);
429 closest_dist = dist;
430 }
431 }
432 }
433 }
434
435 closest
436 })
437 .flatten()
438 }
439
440 pub(crate) fn find_end_gateway_tallest_block(
442 &self,
443 around: BlockPos,
444 dist: i32,
445 allow_bedrock: bool,
446 ) -> BlockPos {
447 let mut tallest = None;
448
449 for dx in -dist..=dist {
450 for dz in -dist..=dist {
451 if dx == 0 && dz == 0 && !allow_bedrock {
452 continue;
453 }
454
455 let min_y = tallest.map_or(self.get_min_y(), |pos: BlockPos| pos.y());
456 for y in (min_y + 1..=self.get_max_y()).rev() {
457 let pos = BlockPos::new(around.x() + dx, y, around.z() + dz);
458 let state = self.get_block_state(pos);
459 if self.is_collision_shape_full_block_at(pos, state)
460 && (allow_bedrock || state.get_block() != &vanilla_blocks::BEDROCK)
461 {
462 tallest = Some(pos);
463 break;
464 }
465 }
466 }
467 }
468
469 tallest.unwrap_or(around)
470 }
471
472 pub(super) fn is_collision_shape_full_block_at(
473 &self,
474 pos: BlockPos,
475 state: BlockStateId,
476 ) -> bool {
477 is_shape_full_block(self.block_collision_shape(pos, state))
478 }
479
480 pub(crate) fn create_end_island(self: &Arc<Self>, origin: BlockPos) -> bool {
482 let end_stone = vanilla_blocks::END_STONE.default_state();
483 let mut random = LegacyRandom::from_seed(PackedBlockPos::from(origin).as_raw() as u64);
484 let mut size = random.next_i32_bounded(3) as f32 + 4.0;
485 let mut y = 0;
486
487 while size > 0.5 {
488 let min = (-size).floor() as i32;
489 let max = size.ceil() as i32;
490 for x in min..=max {
491 for z in min..=max {
492 if (x * x + z * z) as f32 <= (size + 1.0) * (size + 1.0)
493 && !self.set_block(
494 origin.offset(x, y, z),
495 end_stone,
496 UpdateFlags::UPDATE_CLIENTS,
497 )
498 {
499 return false;
500 }
501 }
502 }
503
504 size -= random.next_i32_bounded(2) as f32 + 0.5;
505 y -= 1;
506 }
507
508 true
509 }
510
511 pub(crate) fn create_end_gateway_portal(
513 self: &Arc<Self>,
514 origin: BlockPos,
515 exit: BlockPos,
516 exact: bool,
517 ) -> bool {
518 for dy in -2_i32..=2 {
519 for dx in -1..=1 {
520 for dz in -1..=1 {
521 let same_x = dx == 0;
522 let same_y = dy == 0;
523 let same_z = dz == 0;
524 let end = dy.abs() == 2;
525 let state = if same_x && same_y && same_z {
526 vanilla_blocks::END_GATEWAY.default_state()
527 } else if same_y {
528 vanilla_blocks::AIR.default_state()
529 } else if (end && same_x && same_z) || ((same_x || same_z) && !end) {
530 vanilla_blocks::BEDROCK.default_state()
531 } else {
532 vanilla_blocks::AIR.default_state()
533 };
534
535 if !self.set_block(origin.offset(dx, dy, dz), state, UpdateFlags::UPDATE_ALL) {
536 return false;
537 }
538 }
539 }
540 }
541
542 let Some(block_entity) = self.get_block_entity(origin) else {
543 return false;
544 };
545 let Some(gateway) = block_entity.downcast_ref::<EndGatewayBlockEntity>() else {
546 return false;
547 };
548 gateway.set_exit_position(exit, exact);
549 true
550 }
551
552 pub(super) fn clear_nether_portal_fallback_box(
553 self: &Arc<Self>,
554 origin: BlockPos,
555 direction: Direction,
556 ) -> bool {
557 let obsidian = vanilla_blocks::OBSIDIAN.default_state();
558 let air = vanilla_blocks::AIR.default_state();
559
560 for box_offset in -1..2 {
561 for width in 0..2 {
562 for height in -1..3 {
563 let state = if height < 0 { obsidian } else { air };
564 let pos = nether_portal_frame_offset_pos(
565 origin, direction, width, height, box_offset,
566 );
567 if !self.set_block(pos, state, UpdateFlags::UPDATE_ALL) {
568 return false;
569 }
570 }
571 }
572 }
573
574 true
575 }
576
577 pub(super) fn place_nether_portal_frame_and_blocks(
578 self: &Arc<Self>,
579 origin: BlockPos,
580 direction: Direction,
581 portal_axis: Axis,
582 ) -> bool {
583 let obsidian = vanilla_blocks::OBSIDIAN.default_state();
584 for width in -1..3 {
585 for height in -1..4 {
586 if width == -1 || width == 2 || height == -1 || height == 3 {
587 let pos = nether_portal_frame_offset_pos(origin, direction, width, height, 0);
588 if !self.set_block(pos, obsidian, UpdateFlags::UPDATE_ALL) {
589 return false;
590 }
591 }
592 }
593 }
594
595 let portal_state = vanilla_blocks::NETHER_PORTAL
596 .default_state()
597 .set_value(&BlockStateProperties::HORIZONTAL_AXIS, portal_axis);
598 let portal_flags = UpdateFlags::UPDATE_CLIENTS | UpdateFlags::UPDATE_KNOWN_SHAPE;
599 for width in 0..2 {
600 for height in 0..3 {
601 let pos = nether_portal_frame_offset_pos(origin, direction, width, height, 0);
602 if !self.set_block(pos, portal_state, portal_flags) {
603 return false;
604 }
605 }
606 }
607
608 true
609 }
610}