Skip to main content

steel_core/world/player_spawn_finder/
fallback.rs

1//! The standing-spot fallback for a search in which every candidate failed.
2
3use std::sync::Arc;
4
5use steel_registry::blocks::block_state_ext::BlockStateExt as _;
6use steel_registry::blocks::properties::Direction;
7use steel_registry::blocks::shapes::is_offset_face_full;
8use steel_utils::BlockPos;
9
10use crate::chunk::heightmap::HeightmapType;
11use crate::world::World;
12
13use super::chunk_load::{PollBudget, ScanEnd, Visit};
14use super::{LoadPoll, PlayerSpawnSearch};
15
16/// Looks for a standing spot in the candidate columns, one chunk at a time.
17pub(super) struct FallbackPass {
18    /// Candidate indices whose column was already scanned.
19    scanned: Vec<bool>,
20    /// First candidate index that may still be unscanned.
21    anchor: i32,
22    /// Set once every candidate column was scanned without finding a spot.
23    pub(super) exhausted: bool,
24}
25
26impl FallbackPass {
27    pub(super) fn new(candidates: usize) -> Self {
28        Self {
29            scanned: vec![false; candidates],
30            anchor: 0,
31            exhausted: false,
32        }
33    }
34}
35
36pub(super) enum FallbackPoll {
37    Found(BlockPos),
38    Waiting,
39    Cancelled,
40    Exhausted,
41}
42
43impl PlayerSpawnSearch {
44    /// Scans the candidate columns for a standing spot, loading one chunk at a time.
45    pub(super) fn poll_fallback_pass(
46        &mut self,
47        world: &Arc<World>,
48        budget: &mut PollBudget,
49    ) -> FallbackPoll {
50        let sequence = self.sequence;
51        loop {
52            let (chunk, next_scan) = if let Some(load) = &self.loading {
53                (load.chunk, load.next_scan)
54            } else {
55                let Some(fallback) = self.fallback.as_mut() else {
56                    return FallbackPoll::Exhausted;
57                };
58                let Some(anchor) = (fallback.anchor..sequence.count()).find(|&index| {
59                    fallback
60                        .scanned
61                        .get(index as usize)
62                        .is_some_and(|scanned| !scanned)
63                }) else {
64                    fallback.exhausted = true;
65                    return FallbackPoll::Exhausted;
66                };
67                fallback.anchor = anchor;
68                (sequence.chunk(anchor), anchor)
69            };
70            let mut load = match self.poll_loading(world, chunk, next_scan) {
71                LoadPoll::Ready(load) => load,
72                LoadPoll::Waiting => return FallbackPoll::Waiting,
73                LoadPoll::Cancelled => return FallbackPoll::Cancelled,
74            };
75
76            let Some(fallback) = self.fallback.as_mut() else {
77                return FallbackPoll::Exhausted;
78            };
79            let scanned = &mut fallback.scanned;
80            let end = load.scan(&sequence, budget, |index, x, z| {
81                let Some(scanned) = scanned.get_mut(index as usize) else {
82                    return Visit::Skip;
83                };
84                if *scanned {
85                    return Visit::Skip;
86                }
87                *scanned = true;
88                world
89                    .safe_spawn_in_column(x, z)
90                    .map_or(Visit::Evaluated, Visit::Stop)
91            });
92            match end {
93                ScanEnd::Stopped(pos) => return FallbackPoll::Found(pos),
94                ScanEnd::OutOfBudget => {
95                    self.loading = Some(load);
96                    return FallbackPoll::Waiting;
97                }
98                ScanEnd::Finished => {}
99            }
100        }
101    }
102}
103
104impl World {
105    /// Returns a position in the column where a player can stand: on a block with a full top
106    /// face that holds no fluid, with a free player box above it. In a dimension with a
107    /// ceiling the scan starts below the roof, so the roof is never chosen, and it returns the
108    /// highest such spot. Under an open sky only the surface position is tested.
109    pub(super) fn safe_spawn_in_column(self: &Arc<Self>, x: i32, z: i32) -> Option<BlockPos> {
110        let min_y = self.get_min_y();
111        let top_y = if self.dimension_type.has_ceiling {
112            min_y + self.dimension_type.logical_height - 1
113        } else {
114            self.vanilla_chunk_height_at(HeightmapType::MotionBlocking, x, z)? + 1
115        };
116
117        // Below an open sky only the surface counts; what lies under it is sealed caves.
118        let lowest_y = if self.dimension_type.has_ceiling {
119            min_y + 1
120        } else {
121            top_y
122        };
123
124        for y in (lowest_y..=top_y).rev() {
125            let floor_pos = BlockPos::new(x, y - 1, z);
126            let floor = self.get_block_state(floor_pos);
127            if floor.has_fluid()
128                || !is_offset_face_full(floor.get_collision_shape_at(floor_pos), Direction::Up)
129            {
130                continue;
131            }
132            let pos = BlockPos::new(x, y, z);
133            // Prunes solid blocks before the much costlier box test. It also rejects a head block
134            // whose shape stays above the player's box, which the box test would allow.
135            let head_pos = pos.above();
136            let feet = self.get_block_state(pos);
137            let head = self.get_block_state(head_pos);
138            if feet.has_fluid()
139                || head.has_fluid()
140                || !feet.get_collision_shape_at(pos).is_empty()
141                || !head.get_collision_shape_at(head_pos).is_empty()
142            {
143                continue;
144            }
145            if self.no_collision_no_liquid(pos) {
146                return Some(pos);
147            }
148        }
149
150        None
151    }
152}