steel_core/world/player_spawn_finder/
fallback.rs1use 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
16pub(super) struct FallbackPass {
18 scanned: Vec<bool>,
20 anchor: i32,
22 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 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 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 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 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}