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 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 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}