1use std::collections::VecDeque;
4use std::env;
5use std::sync::Arc;
6use std::time::{Duration, Instant};
7
8use steel_utils::{ChunkPos, Identifier, SectionPos};
9use tokio::time::sleep;
10use tokio_util::sync::CancellationToken;
11
12use crate::chunk::chunk_pyramid::GENERATION_PYRAMID;
13use crate::chunk::chunk_request::{
14 ChunkRequest, ChunkRequestHandle, ChunkRequestState, ChunkTicketKind,
15};
16use crate::chunk::status::ChunkStatus;
17use crate::server::Server;
18use crate::world::World;
19
20#[cfg(feature = "slow_chunk_gen")]
21use crate::chunk::chunk_holder::SLOW_CHUNK_GEN;
22#[cfg(feature = "slow_chunk_gen")]
23use std::sync::atomic::Ordering;
24
25const PREGEN_SIZE_ENV: &str = "PREGEN_SIZE";
26const WORLD_IDENTIFIER_ENV: &str = "WORLD_IDENTIFIER";
27const PREGEN_WINDOW_SIZE_ENV: &str = "PREGEN_WINDOW_SIZE";
28const VANILLA_PLAYER_SPAWN_SIZE_CHUNKS: i32 = 7;
29const DEFAULT_PREGEN_WINDOW_SIZE: i32 = 32;
30const PREGEN_ACTIVE_WINDOWS: usize = 2;
31const PREGEN_UNLOAD_BACKPRESSURE_HIGH: usize = 8192;
32const PREGEN_UNLOAD_BACKPRESSURE_LOW: usize = 4096;
33const FULL_DEPENDENCY_RADIUS: i32 = GENERATION_PYRAMID
34 .get_step_to(ChunkStatus::Full)
35 .accumulated_dependencies
36 .get_radius_of(ChunkStatus::Empty) as i32;
37
38#[derive(Clone, Copy, Debug)]
39struct PregenWindow {
40 min_x: i32,
41 max_x: i32,
42 min_z: i32,
43 max_z: i32,
44}
45
46impl PregenWindow {
47 fn positions(self) -> Vec<ChunkPos> {
48 let mut positions = Vec::with_capacity(self.chunk_count());
49 for z in self.min_z..=self.max_z {
50 for x in self.min_x..=self.max_x {
51 positions.push(ChunkPos::new(x, z));
52 }
53 }
54 positions
55 }
56
57 const fn chunk_count(self) -> usize {
58 (self.width() * self.height()) as usize
59 }
60
61 const fn width(self) -> i32 {
62 self.max_x - self.min_x + 1
63 }
64
65 const fn height(self) -> i32 {
66 self.max_z - self.min_z + 1
67 }
68
69 const fn protected_rect(self) -> PregenRect {
70 PregenRect {
71 min_x: self.min_x - FULL_DEPENDENCY_RADIUS,
72 max_x: self.max_x + FULL_DEPENDENCY_RADIUS,
73 min_z: self.min_z - FULL_DEPENDENCY_RADIUS,
74 max_z: self.max_z + FULL_DEPENDENCY_RADIUS,
75 }
76 }
77}
78
79#[derive(Clone, Copy)]
80struct PregenRect {
81 min_x: i32,
82 max_x: i32,
83 min_z: i32,
84 max_z: i32,
85}
86
87impl PregenRect {
88 const fn overlaps(self, other: Self) -> bool {
89 self.min_x <= other.max_x
90 && self.max_x >= other.min_x
91 && self.min_z <= other.max_z
92 && self.max_z >= other.min_z
93 }
94}
95
96struct ActivePregenWindow {
97 window: PregenWindow,
98 handle: ChunkRequestHandle,
99 ready_chunks: usize,
100 ready: bool,
101 counted: bool,
102}
103
104impl ActivePregenWindow {
105 fn new(world: &Arc<World>, window: PregenWindow) -> Self {
106 let handle = world.chunk_map.request_chunks(ChunkRequest {
107 status: ChunkStatus::Full,
108 positions: window.positions(),
109 ticket_kind: ChunkTicketKind::Pregen,
110 });
111 Self {
112 window,
113 handle,
114 ready_chunks: 0,
115 ready: false,
116 counted: false,
117 }
118 }
119
120 fn poll(&mut self, world: &Arc<World>) {
121 match self.handle.poll() {
122 ChunkRequestState::Ready => {
123 self.ready_chunks = self.window.chunk_count();
124 self.ready = true;
125 }
126 ChunkRequestState::Pending { ready, .. } => {
127 self.ready_chunks = ready;
128 }
129 ChunkRequestState::Cancelled => {
130 self.handle = world.chunk_map.request_chunks(ChunkRequest {
131 status: ChunkStatus::Full,
132 positions: self.window.positions(),
133 ticket_kind: ChunkTicketKind::Pregen,
134 });
135 self.ready_chunks = 0;
136 self.ready = false;
137 self.counted = false;
138 }
139 }
140 }
141}
142
143#[derive(Clone, Copy, Debug, PartialEq, Eq)]
144struct PregenSize {
145 side_length: i32,
146 radius: i32,
147}
148
149impl PregenSize {
150 fn from_side_length(side_length: i32) -> Result<Option<Self>, String> {
151 if side_length == 0 {
152 return Ok(None);
153 }
154 if side_length < 0 {
155 return Err(format!(
156 "{PREGEN_SIZE_ENV} must be 0 or a positive odd integer"
157 ));
158 }
159 if side_length % 2 == 0 {
160 return Err(format!(
161 "{PREGEN_SIZE_ENV} must be odd so the area has a single center chunk"
162 ));
163 }
164
165 Ok(Some(Self {
166 side_length,
167 radius: side_length / 2,
168 }))
169 }
170}
171
172impl Server {
173 pub async fn prepare_spawn_area(&self) -> bool {
180 let overworld = match get_world_identifier() {
181 Ok(None) => self.overworld(),
182 Ok(Some(identifier)) => {
183 let Some(world) = self.worlds.get(&identifier) else {
184 log::error!("{WORLD_IDENTIFIER_ENV} references unknown world {identifier}");
185 return false;
186 };
187 world
188 }
189 Err(error) => {
190 log::error!("{error}");
191 return false;
192 }
193 };
194 let pregen_size = match get_pregen_size() {
195 Ok(Some(size)) => size,
196 Ok(None) => {
197 log::info!("Skipping custom startup spawn-area pregeneration");
198 return true;
199 }
200 Err(error) => {
201 log::error!("{error}");
202 return false;
203 }
204 };
205 let window_size = match get_pregen_window_size() {
206 Ok(window_size) => window_size,
207 Err(error) => {
208 log::error!("{error}");
209 return false;
210 }
211 };
212 let center_chunk = if pregen_size.side_length > VANILLA_PLAYER_SPAWN_SIZE_CHUNKS {
213 ChunkPos::new(0, 0)
214 } else {
215 let spawn_pos = overworld.level_data.read().data().spawn_pos();
216 ChunkPos::new(
217 SectionPos::block_to_section_coord(spawn_pos.0.x),
218 SectionPos::block_to_section_coord(spawn_pos.0.z),
219 )
220 };
221
222 pregen_overworld(
223 overworld,
224 center_chunk,
225 pregen_size,
226 window_size,
227 &self.cancel_token,
228 )
229 .await
230 }
231}
232
233fn get_pregen_size() -> Result<Option<PregenSize>, String> {
234 let side_length = match env::var(PREGEN_SIZE_ENV) {
235 Ok(value) => value
236 .parse::<i32>()
237 .map_err(|e| format!("{PREGEN_SIZE_ENV} must be 0 or a positive odd integer: {e}"))?,
238 Err(env::VarError::NotPresent) => return Ok(None),
239 Err(env::VarError::NotUnicode(_)) => {
240 return Err(format!("{PREGEN_SIZE_ENV} must be valid unicode"));
241 }
242 };
243
244 PregenSize::from_side_length(side_length)
245}
246
247fn get_world_identifier() -> Result<Option<Identifier>, String> {
248 match env::var(WORLD_IDENTIFIER_ENV) {
249 Ok(value) => value
250 .parse::<Identifier>()
251 .map(Some)
252 .map_err(|e| format!("{WORLD_IDENTIFIER_ENV} must be a valid identifier: {e}")),
253 Err(env::VarError::NotPresent) => Ok(None),
254 Err(env::VarError::NotUnicode(_)) => {
255 Err(format!("{WORLD_IDENTIFIER_ENV} must be valid unicode"))
256 }
257 }
258}
259
260fn get_pregen_window_size() -> Result<i32, String> {
261 match env::var(PREGEN_WINDOW_SIZE_ENV) {
262 Ok(value) => parse_pregen_window_size(&value),
263 Err(env::VarError::NotPresent) => Ok(DEFAULT_PREGEN_WINDOW_SIZE),
264 Err(env::VarError::NotUnicode(_)) => {
265 Err(format!("{PREGEN_WINDOW_SIZE_ENV} must be valid unicode"))
266 }
267 }
268}
269
270fn parse_pregen_window_size(value: &str) -> Result<i32, String> {
271 let window_size = value
272 .parse::<i32>()
273 .map_err(|error| format!("{PREGEN_WINDOW_SIZE_ENV} must be a positive integer: {error}"))?;
274 if window_size <= 0 {
275 return Err(format!(
276 "{PREGEN_WINDOW_SIZE_ENV} must be a positive integer"
277 ));
278 }
279
280 let window_size_as_usize = window_size as usize;
281 let Some(active_target_chunks) = window_size_as_usize
282 .checked_mul(window_size_as_usize)
283 .and_then(|chunk_count| chunk_count.checked_mul(PREGEN_ACTIVE_WINDOWS))
284 else {
285 return Err(format!(
286 "{PREGEN_WINDOW_SIZE_ENV} is too large for the pregeneration window budget"
287 ));
288 };
289 if active_target_chunks > PREGEN_UNLOAD_BACKPRESSURE_HIGH {
290 return Err(format!(
291 "{PREGEN_WINDOW_SIZE_ENV} must keep {PREGEN_ACTIVE_WINDOWS} active windows within the {PREGEN_UNLOAD_BACKPRESSURE_HIGH}-chunk unload-backpressure budget"
292 ));
293 }
294
295 Ok(window_size)
296}
297
298async fn pregen_overworld(
299 world: &Arc<World>,
300 center_chunk: ChunkPos,
301 pregen_size: PregenSize,
302 window_size: i32,
303 cancel_token: &CancellationToken,
304) -> bool {
305 let total_chunks = total_chunks(pregen_size.side_length);
306
307 log::info!(
308 "Preparing spawn area: {} chunks ({}x{}) around chunk ({}, {})",
309 total_chunks,
310 pregen_size.side_length,
311 pregen_size.side_length,
312 center_chunk.0.x,
313 center_chunk.0.y,
314 );
315
316 #[cfg(feature = "slow_chunk_gen")]
317 SLOW_CHUNK_GEN.store(true, Ordering::Relaxed);
318
319 let elapsed = {
320 let start = Instant::now();
321 let completed =
322 generate_pregen(world, center_chunk, pregen_size, window_size, cancel_token).await;
323 (start.elapsed(), completed)
324 };
325
326 #[cfg(feature = "slow_chunk_gen")]
327 SLOW_CHUNK_GEN.store(false, Ordering::Relaxed);
328
329 let elapsed_secs = elapsed.0.as_secs_f64();
330 let chunks_per_second = if elapsed_secs > 0.0 {
331 total_chunks as f64 / elapsed_secs
332 } else {
333 0.0
334 };
335 if elapsed.1 {
336 log::info!(
337 "Spawn area prepared: {total_chunks} chunks in {elapsed_secs:.2}s ({chunks_per_second:.1} chunks/s)",
338 );
339 } else {
340 log::info!("Spawn area preparation cancelled after {elapsed_secs:.2}s");
341 }
342 elapsed.1
343}
344
345fn build_pregen_windows(
346 center_chunk: ChunkPos,
347 radius: i32,
348 window_size: i32,
349) -> VecDeque<PregenWindow> {
350 let min_x = center_chunk.0.x - radius;
351 let max_x = center_chunk.0.x + radius;
352 let min_z = center_chunk.0.y - radius;
353 let max_z = center_chunk.0.y + radius;
354 let x_ranges = pregen_window_ranges(min_x, max_x, window_size);
355 let z_ranges = pregen_window_ranges(min_z, max_z, window_size);
356 let mut windows = VecDeque::new();
357
358 for (strip_index, z_pair) in z_ranges.chunks(2).enumerate() {
361 let mut push_column = |&(window_min_x, window_max_x): &(i32, i32)| {
362 for &(window_min_z, window_max_z) in z_pair {
363 windows.push_back(PregenWindow {
364 min_x: window_min_x,
365 max_x: window_max_x,
366 min_z: window_min_z,
367 max_z: window_max_z,
368 });
369 }
370 };
371
372 if strip_index % 2 == 0 {
373 for x_range in &x_ranges {
374 push_column(x_range);
375 }
376 } else {
377 for x_range in x_ranges.iter().rev() {
378 push_column(x_range);
379 }
380 }
381 }
382
383 windows
384}
385
386fn pregen_window_ranges(min: i32, max: i32, window_size: i32) -> Vec<(i32, i32)> {
387 let mut ranges = Vec::new();
388 let mut start = min;
389 while start <= max {
390 let end = start.saturating_add(window_size - 1).min(max);
391 ranges.push((start, end));
392 start = end + 1;
393 }
394 ranges
395}
396
397async fn generate_pregen(
398 world: &Arc<World>,
399 center_chunk: ChunkPos,
400 pregen_size: PregenSize,
401 window_size: i32,
402 cancel_token: &CancellationToken,
403) -> bool {
404 let total_chunks = total_chunks(pregen_size.side_length);
405 let mut pending_windows = build_pregen_windows(center_chunk, pregen_size.radius, window_size);
406 let mut active_windows = Vec::with_capacity(PREGEN_ACTIVE_WINDOWS + 1);
407 let mut last_report = Instant::now();
408 let mut last_completed = 0usize;
409 let mut completed = 0usize;
410 let mut unload_backpressure = false;
411 let mut peak_unloading_chunks = 0usize;
412 let start = Instant::now();
413
414 log::info!(
415 "Pregeneration windowing: {window_size}x{window_size} target chunks, {PREGEN_ACTIVE_WINDOWS} active windows, dependency halo {FULL_DEPENDENCY_RADIUS} chunks",
416 );
417
418 fill_active_windows(world, &mut pending_windows, &mut active_windows);
419
420 while completed < total_chunks {
421 if cancel_token.is_cancelled() {
422 release_all_windows(world, &mut active_windows);
423 return false;
424 }
425
426 drain_pregen_broadcasts(world);
427 world.chunk_map.advance_scheduling();
428 peak_unloading_chunks = peak_unloading_chunks.max(world.chunk_map.unloading_chunks.len());
429 update_unload_backpressure(world, &mut unload_backpressure);
430
431 for active in &mut active_windows {
432 active.poll(world);
433 }
434
435 if !unload_backpressure {
436 let newly_ready_count = active_windows
437 .iter()
438 .filter(|active| active.ready && !active.counted)
439 .count();
440 for _ in 0..newly_ready_count {
441 activate_next_window(world, &mut pending_windows, &mut active_windows);
442 }
443 }
444
445 for active in &mut active_windows {
446 if active.ready && !active.counted {
447 completed += active.window.chunk_count();
448 active.counted = true;
449 }
450 }
451 if !unload_backpressure {
452 fill_active_windows(world, &mut pending_windows, &mut active_windows);
453 }
454 drain_pregen_broadcasts(world);
455 world.chunk_map.advance_scheduling();
456 release_unneeded_completed_windows(world, &mut active_windows);
457 peak_unloading_chunks = peak_unloading_chunks.max(world.chunk_map.unloading_chunks.len());
458 update_unload_backpressure(world, &mut unload_backpressure);
459
460 if completed == total_chunks {
461 break;
462 }
463
464 if pregen_size.side_length > VANILLA_PLAYER_SPAWN_SIZE_CHUNKS
465 && last_report.elapsed() >= Duration::from_secs(5)
466 {
467 let report_elapsed = last_report.elapsed().as_secs_f64();
468 let ready_in_active = active_windows
469 .iter()
470 .filter(|active| !active.counted)
471 .map(|active| active.ready_chunks)
472 .sum::<usize>();
473 let current_completed = (completed + ready_in_active).min(total_chunks);
474 let elapsed = start.elapsed().as_secs_f64();
475 let chunks_per_sec = if elapsed > 0.0 {
476 (current_completed.saturating_sub(last_completed)) as f64 / report_elapsed
477 } else {
478 0.0
479 };
480 let percent = (current_completed as f64 / total_chunks as f64) * 100.0;
481 let remaining = total_chunks.saturating_sub(current_completed);
482 let eta = if chunks_per_sec > 0.0 && remaining > 0 {
483 remaining as f64 / chunks_per_sec
484 } else {
485 0.0
486 };
487 log::info!(
488 "Progress: {current_completed}/{total_chunks} ({percent:.1}%), {chunks_per_sec:.1} chunks/s, ETA: {eta:.0}s",
489 );
490 last_report = Instant::now();
491 last_completed = current_completed;
492 }
493
494 tokio::select! {
495 () = cancel_token.cancelled() => {
496 release_all_windows(world, &mut active_windows);
497 return false;
498 }
499 () = sleep(Duration::from_millis(10)) => {}
500 }
501 }
502
503 release_all_windows(world, &mut active_windows);
504 peak_unloading_chunks = peak_unloading_chunks.max(world.chunk_map.unloading_chunks.len());
505 log::info!("Pregeneration peak unload backlog: {peak_unloading_chunks} chunks");
506 true
507}
508
509fn update_unload_backpressure(world: &Arc<World>, unload_backpressure: &mut bool) {
510 let unloading_chunks = world.chunk_map.unloading_chunks.len();
511 if *unload_backpressure {
512 if unloading_chunks <= PREGEN_UNLOAD_BACKPRESSURE_LOW {
513 *unload_backpressure = false;
514 log::info!(
515 "Pregen unload backpressure released: unloading_chunks={unloading_chunks}, low_watermark={PREGEN_UNLOAD_BACKPRESSURE_LOW}",
516 );
517 }
518 return;
519 }
520
521 if unloading_chunks >= PREGEN_UNLOAD_BACKPRESSURE_HIGH {
522 *unload_backpressure = true;
523 log::info!(
524 "Pregen unload backpressure active: unloading_chunks={unloading_chunks}, high_watermark={PREGEN_UNLOAD_BACKPRESSURE_HIGH}, low_watermark={PREGEN_UNLOAD_BACKPRESSURE_LOW}",
525 );
526 }
527}
528
529fn drain_pregen_broadcasts(world: &Arc<World>) {
530 world.chunk_map.broadcast_changed_chunks();
531}
532
533fn total_chunks(side_length: i32) -> usize {
534 let side_length = i64::from(side_length);
535 (side_length * side_length) as usize
536}
537
538fn fill_active_windows(
539 world: &Arc<World>,
540 pending_windows: &mut VecDeque<PregenWindow>,
541 active_windows: &mut Vec<ActivePregenWindow>,
542) {
543 while active_windows.iter().filter(|active| !active.ready).count() < PREGEN_ACTIVE_WINDOWS {
544 if !activate_next_window(world, pending_windows, active_windows) {
545 break;
546 }
547 }
548}
549
550fn activate_next_window(
551 world: &Arc<World>,
552 pending_windows: &mut VecDeque<PregenWindow>,
553 active_windows: &mut Vec<ActivePregenWindow>,
554) -> bool {
555 let Some(window) = pending_windows.pop_front() else {
556 return false;
557 };
558
559 active_windows.push(ActivePregenWindow::new(world, window));
560 true
561}
562
563fn release_unneeded_completed_windows(
564 world: &Arc<World>,
565 active_windows: &mut Vec<ActivePregenWindow>,
566) {
567 let incomplete_windows = active_windows
568 .iter()
569 .filter(|active| !active.ready)
570 .map(|active| active.window)
571 .collect::<Vec<_>>();
572
573 active_windows.retain(|active| {
574 if !active.ready {
575 return true;
576 }
577
578 let protected = active.window.protected_rect();
579
580 incomplete_windows
581 .iter()
582 .any(|window| protected.overlaps(window.protected_rect()))
583 });
584
585 world.chunk_map.advance_scheduling();
586}
587
588fn release_all_windows(world: &Arc<World>, active_windows: &mut Vec<ActivePregenWindow>) {
589 active_windows.clear();
590 world.chunk_map.advance_scheduling();
591}
592
593#[cfg(test)]
594mod tests {
595 use super::*;
596
597 #[test]
598 fn pregen_size_accepts_zero_as_disabled() {
599 assert_eq!(PregenSize::from_side_length(0), Ok(None));
600 }
601
602 #[test]
603 fn pregen_size_accepts_odd_side_lengths() {
604 assert_eq!(
605 PregenSize::from_side_length(7),
606 Ok(Some(PregenSize {
607 side_length: 7,
608 radius: 3,
609 }))
610 );
611 }
612
613 #[test]
614 fn pregen_size_rejects_even_side_lengths() {
615 assert!(PregenSize::from_side_length(2).is_err());
616 }
617
618 #[test]
619 fn pregen_size_rejects_negative_side_lengths() {
620 assert!(PregenSize::from_side_length(-1).is_err());
621 }
622
623 #[test]
624 fn pregen_window_order_keeps_consecutive_dependency_areas_local() {
625 let radius = DEFAULT_PREGEN_WINDOW_SIZE * 2;
626 let windows = build_pregen_windows(ChunkPos::new(0, 0), radius, DEFAULT_PREGEN_WINDOW_SIZE);
627
628 assert_eq!(windows.len(), 25);
629 assert_eq!(
630 windows
631 .iter()
632 .map(|window| window.chunk_count())
633 .sum::<usize>(),
634 total_chunks(radius * 2 + 1)
635 );
636 assert!(
637 windows
638 .iter()
639 .zip(windows.iter().skip(1))
640 .all(|(current, next)| current.protected_rect().overlaps(next.protected_rect()))
641 );
642 }
643
644 #[test]
645 fn pregen_window_size_requires_a_positive_integer() {
646 assert_eq!(parse_pregen_window_size("1"), Ok(1));
647 assert_eq!(parse_pregen_window_size("64"), Ok(64));
648 assert!(parse_pregen_window_size("0").is_err());
649 assert!(parse_pregen_window_size("-1").is_err());
650 assert!(parse_pregen_window_size("wide").is_err());
651 }
652
653 #[test]
654 fn pregen_window_size_must_fit_unload_backpressure_budget() {
655 assert!(parse_pregen_window_size("65").is_err());
656 assert!(parse_pregen_window_size(&i32::MAX.to_string()).is_err());
657 }
658}