Skip to main content

steel_core/world/
player_spawn_finder.rs

1use std::sync::Arc;
2use std::time::Duration;
3
4use glam::DVec3;
5use steel_registry::vanilla_entities;
6use steel_registry::vanilla_game_rules::RESPAWN_RADIUS;
7use steel_utils::{BlockPos, ChunkPos, SectionPos, WorldAabb, types::GameType};
8use tokio::time::sleep;
9
10use crate::behavior::BlockCollisionContext;
11use crate::chunk::chunk_request::{ChunkRequestHandle, ChunkRequestState, ChunkTicketKind};
12use crate::chunk::status::ChunkStatus;
13use crate::fluid::get_fluid_state;
14use crate::physics::{CollisionWorld as _, WorldCollisionProvider};
15use crate::world::World;
16
17const ABSOLUTE_MAX_ATTEMPTS: i32 = 1024;
18const PLAYER_SPAWN_CHUNK_RADIUS: u8 = 3;
19const CHUNK_REQUEST_POLL_DELAY: Duration = Duration::from_millis(10);
20
21pub(crate) enum PlayerSpawnSearchPoll {
22    Pending,
23    Ready(DVec3),
24    Cancelled,
25}
26
27pub(crate) struct PlayerSpawnSearch {
28    spawn_suggestion: BlockPos,
29    radius: i32,
30    candidate_count: i32,
31    coprime: i32,
32    offset: i32,
33    next_candidate_index: i32,
34    pending: Option<PendingSpawnCandidate>,
35}
36
37struct PendingSpawnCandidate {
38    x: i32,
39    z: i32,
40    kind: SpawnCandidateKind,
41    request: ChunkRequestHandle,
42}
43
44#[derive(Clone, Copy)]
45enum SpawnCandidateKind {
46    Candidate,
47    Fixup,
48}
49
50impl PlayerSpawnSearch {
51    pub(crate) fn new(
52        world: &Arc<World>,
53        spawn_suggestion: BlockPos,
54        game_type: GameType,
55    ) -> Result<Self, String> {
56        if game_type == GameType::Adventure {
57            return Ok(Self {
58                spawn_suggestion,
59                radius: 0,
60                candidate_count: 0,
61                coprime: 0,
62                offset: 0,
63                next_candidate_index: 0,
64                pending: None,
65            });
66        }
67
68        let mut radius = world.get_game_rule(&RESPAWN_RADIUS).max(0);
69        let border_distance = world
70            .world_border_snapshot()
71            .distance_to_border(
72                f64::from(spawn_suggestion.x()),
73                f64::from(spawn_suggestion.z()),
74            )
75            .floor() as i32;
76        if border_distance < radius {
77            radius = border_distance;
78        }
79        if border_distance <= 1 {
80            radius = 1;
81        }
82
83        let square_side = i64::from(radius) * 2 + 1;
84        let candidate_count =
85            i32::try_from(i64::from(ABSOLUTE_MAX_ATTEMPTS).min(square_side * square_side))
86                .map_err(|e| format!("invalid spawn candidate count: {e}"))?;
87        let coprime = get_coprime(candidate_count);
88        let offset = rand::random_range(0..candidate_count);
89
90        Ok(Self {
91            spawn_suggestion,
92            radius,
93            candidate_count,
94            coprime,
95            offset,
96            next_candidate_index: 0,
97            pending: None,
98        })
99    }
100
101    #[must_use]
102    pub(crate) fn poll(&mut self, world: &Arc<World>) -> PlayerSpawnSearchPoll {
103        self.poll_with_ready_candidate_budget(world, usize::MAX)
104    }
105
106    #[must_use]
107    pub(crate) fn poll_with_ready_candidate_budget(
108        &mut self,
109        world: &Arc<World>,
110        ready_candidate_budget: usize,
111    ) -> PlayerSpawnSearchPoll {
112        let ready_candidate_budget = ready_candidate_budget.max(1);
113        let mut ready_candidates_checked = 0;
114
115        loop {
116            if let Some(pending) = &self.pending {
117                match pending.request.poll() {
118                    ChunkRequestState::Pending { .. } => return PlayerSpawnSearchPoll::Pending,
119                    ChunkRequestState::Cancelled => return PlayerSpawnSearchPoll::Cancelled,
120                    ChunkRequestState::Ready => {
121                        if pending.request.ready_chunks().is_none() {
122                            return PlayerSpawnSearchPoll::Pending;
123                        }
124                    }
125                }
126            }
127
128            if let Some(pending) = self.pending.take() {
129                ready_candidates_checked += 1;
130                match pending.kind {
131                    SpawnCandidateKind::Candidate => {
132                        let Some(spawn_pos) = world.level_respawn_pos(pending.x, pending.z) else {
133                            if ready_candidates_checked >= ready_candidate_budget {
134                                self.pending = Some(self.next_candidate(world));
135                                return PlayerSpawnSearchPoll::Pending;
136                            }
137                            continue;
138                        };
139                        if world.no_collision_no_liquid(spawn_pos) {
140                            return PlayerSpawnSearchPoll::Ready(block_bottom_center(spawn_pos));
141                        }
142                        if ready_candidates_checked >= ready_candidate_budget {
143                            self.pending = Some(self.next_candidate(world));
144                            return PlayerSpawnSearchPoll::Pending;
145                        }
146                    }
147                    SpawnCandidateKind::Fixup => {
148                        return PlayerSpawnSearchPoll::Ready(
149                            world.fixup_spawn_height(self.spawn_suggestion),
150                        );
151                    }
152                }
153            }
154
155            self.pending = Some(self.next_candidate(world));
156        }
157    }
158
159    fn next_candidate(&mut self, world: &Arc<World>) -> PendingSpawnCandidate {
160        if self.next_candidate_index < self.candidate_count {
161            let candidate_index = self.next_candidate_index;
162            self.next_candidate_index += 1;
163
164            let value = (self.offset + self.coprime * candidate_index) % self.candidate_count;
165            let delta_x = value % (self.radius * 2 + 1);
166            let delta_z = value / (self.radius * 2 + 1);
167            let target_x = self.spawn_suggestion.x() + delta_x - self.radius;
168            let target_z = self.spawn_suggestion.z() + delta_z - self.radius;
169
170            return PendingSpawnCandidate {
171                x: target_x,
172                z: target_z,
173                kind: SpawnCandidateKind::Candidate,
174                request: world.request_spawn_candidate_chunk(target_x, target_z),
175            };
176        }
177
178        PendingSpawnCandidate {
179            x: self.spawn_suggestion.x(),
180            z: self.spawn_suggestion.z(),
181            kind: SpawnCandidateKind::Fixup,
182            request: world.request_spawn_candidate_chunk(
183                self.spawn_suggestion.x(),
184                self.spawn_suggestion.z(),
185            ),
186        }
187    }
188}
189
190impl World {
191    /// Finds the adjusted shared spawn position used for players entering this world's default spawn.
192    pub async fn find_adjusted_shared_spawn_pos(
193        self: &Arc<Self>,
194        spawn_suggestion: BlockPos,
195        game_type: GameType,
196    ) -> Result<DVec3, String> {
197        let mut search = PlayerSpawnSearch::new(self, spawn_suggestion, game_type)?;
198        loop {
199            match search.poll(self) {
200                PlayerSpawnSearchPoll::Pending => sleep(CHUNK_REQUEST_POLL_DELAY).await,
201                PlayerSpawnSearchPoll::Ready(position) => return Ok(position),
202                PlayerSpawnSearchPoll::Cancelled => {
203                    return Err("spawn search chunk request was cancelled".to_owned());
204                }
205            }
206        }
207    }
208
209    /// Loads the vanilla radius-3 full chunk square around a prepared player spawn.
210    pub async fn prepare_player_spawn_chunks(
211        self: &Arc<Self>,
212        spawn_position: DVec3,
213    ) -> Result<ChunkRequestHandle, String> {
214        let request = self.request_player_spawn_chunks(spawn_position);
215        Self::wait_for_chunk_request(&request).await?;
216        Ok(request)
217    }
218
219    pub(crate) fn request_player_spawn_chunks(
220        self: &Arc<Self>,
221        spawn_position: DVec3,
222    ) -> ChunkRequestHandle {
223        let spawn_pos = BlockPos::containing(spawn_position.x, spawn_position.y, spawn_position.z);
224        let center = ChunkPos::new(
225            SectionPos::block_to_section_coord(spawn_pos.x()),
226            SectionPos::block_to_section_coord(spawn_pos.z()),
227        );
228        self.chunk_map.request_square(
229            center,
230            PLAYER_SPAWN_CHUNK_RADIUS,
231            ChunkStatus::Full,
232            ChunkTicketKind::PlayerSpawn,
233        )
234    }
235
236    fn request_spawn_candidate_chunk(self: &Arc<Self>, x: i32, z: i32) -> ChunkRequestHandle {
237        let chunk = ChunkPos::new(
238            SectionPos::block_to_section_coord(x),
239            SectionPos::block_to_section_coord(z),
240        );
241        self.chunk_map
242            .request_chunk(chunk, ChunkStatus::Full, ChunkTicketKind::SpawnSearch)
243    }
244
245    async fn wait_for_chunk_request(request: &ChunkRequestHandle) -> Result<(), String> {
246        loop {
247            match request.poll() {
248                ChunkRequestState::Ready => return Ok(()),
249                ChunkRequestState::Cancelled => {
250                    return Err("chunk request was cancelled".to_owned());
251                }
252                ChunkRequestState::Pending { .. } => {
253                    sleep(CHUNK_REQUEST_POLL_DELAY).await;
254                }
255            }
256        }
257    }
258
259    fn fixup_spawn_height(self: &Arc<Self>, spawn_pos: BlockPos) -> DVec3 {
260        let mut pos = spawn_pos;
261
262        while !self.no_collision_no_liquid(pos) && pos.y() < self.get_max_y() {
263            pos = pos.above();
264        }
265
266        pos = pos.below();
267
268        while self.no_collision_no_liquid(pos) && pos.y() > self.get_min_y() {
269            pos = pos.below();
270        }
271
272        block_bottom_center(pos.above())
273    }
274
275    fn no_collision_no_liquid(self: &Arc<Self>, pos: BlockPos) -> bool {
276        let dimensions = vanilla_entities::PLAYER.dimensions;
277        let aabb = WorldAabb::entity_box(
278            f64::from(pos.x()) + 0.5,
279            f64::from(pos.y()),
280            f64::from(pos.z()) + 0.5,
281            f64::from(dimensions.half_width()),
282            f64::from(dimensions.height),
283        );
284        let collision_world = WorldCollisionProvider::new(self);
285
286        !collision_world.has_entity_collision(&aabb)
287            && !collision_world
288                .has_block_collision_with_context(&aabb, BlockCollisionContext::empty())
289            && !aabb_contains_any_liquid(self, aabb)
290    }
291}
292
293fn aabb_contains_any_liquid(world: &Arc<World>, aabb: WorldAabb) -> bool {
294    let min_x = aabb.min_x().floor() as i32;
295    let max_x = aabb.max_x().ceil() as i32;
296    let min_y = aabb.min_y().floor() as i32;
297    let max_y = aabb.max_y().ceil() as i32;
298    let min_z = aabb.min_z().floor() as i32;
299    let max_z = aabb.max_z().ceil() as i32;
300
301    for x in min_x..max_x {
302        for y in min_y..max_y {
303            for z in min_z..max_z {
304                if !get_fluid_state(world, BlockPos::new(x, y, z)).is_empty() {
305                    return true;
306                }
307            }
308        }
309    }
310
311    false
312}
313
314const fn get_coprime(possible_origins: i32) -> i32 {
315    if possible_origins <= 16 {
316        possible_origins - 1
317    } else {
318        17
319    }
320}
321
322fn block_bottom_center(pos: BlockPos) -> DVec3 {
323    let (x, y, z) = pos.get_bottom_center();
324    DVec3::new(x, y, z)
325}