1use futures::Future;
3use rustc_hash::FxHashSet;
4use std::fmt::Debug;
5use std::mem;
6use std::sync::atomic::{AtomicBool, AtomicU8, AtomicU64, AtomicUsize, Ordering};
7use std::sync::{Arc, OnceLock, Weak};
8use steel_utils::{BlockPos, ChunkPos, PackedSectionBlockPos, SectionPos, locks::SyncMutex};
9use tokio::sync::{Notify, oneshot};
10#[cfg(feature = "slow_chunk_gen")]
11use tokio::time::sleep;
12
13#[cfg(feature = "slow_chunk_gen")]
14use std::time::Duration;
15
16#[cfg(feature = "slow_chunk_gen")]
19pub static SLOW_CHUNK_GEN: AtomicBool = AtomicBool::new(false);
20
21use crate::chunk::chunk_generation_task::{NeighborReady, StaticCache2D};
22use crate::chunk::chunk_ticket_manager::{
23 ChunkTicketLevel, generation_status, is_entity_ticking, is_full,
24};
25use crate::chunk::full_chunk_readiness::FullPublicationQueue;
26use crate::chunk::light::{
27 LightLayer, LightSectionRange, LightWorkWindowGate, LightWorkWindowReservation,
28};
29use crate::chunk_saver::ChunkStorage;
30use crate::entity::EntityVisibility;
31use crate::worldgen::WorldGenContext;
32use crate::{
33 ChunkMap,
34 chunk::{
35 Chunk,
36 chunk_generation_task::ChunkGenerationTask,
37 chunk_pyramid::ChunkStep,
38 full_chunk::{FullChunkPromotion, FullChunkRef},
39 status::ChunkStatus,
40 },
41};
42
43const STATUS_NONE: u8 = u8::MAX;
44const UNPUBLISHED_STATUS: u8 = 0;
45const NO_TICKET_LEVEL: u8 = u8::MAX;
46const SAVE_LIFECYCLE_ACTIVE: u8 = 0;
47const SAVE_LIFECYCLE_UNLOADING: u8 = 1;
48const SAVE_LIFECYCLE_PREPARING: u8 = 2;
49
50fn optional_ticket_level_raw(level: Option<ChunkTicketLevel>) -> u8 {
51 level.map_or(NO_TICKET_LEVEL, ChunkTicketLevel::raw)
52}
53
54const fn optional_ticket_level_from_raw(raw: u8) -> Option<ChunkTicketLevel> {
55 if raw == NO_TICKET_LEVEL {
56 None
57 } else {
58 ChunkTicketLevel::new(raw)
59 }
60}
61
62const fn encoded_published_status(status: ChunkStatus) -> u8 {
63 status.get_index() as u8 + 1
64}
65
66fn decoded_published_status(status: u8) -> Option<ChunkStatus> {
67 if status == UNPUBLISHED_STATUS {
68 return None;
69 }
70
71 let decoded = ChunkStatus::from_index(usize::from(status - 1));
72 assert!(
73 decoded.is_some(),
74 "invalid published chunk status: {status}"
75 );
76 decoded
77}
78
79#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
80#[repr(u8)]
81pub(crate) enum TickingReadiness {
82 Unready,
83 BlockTicking,
84 EntityTicking,
85}
86
87#[derive(Debug, Clone, Copy, PartialEq, Eq)]
89pub(crate) struct TickingReadinessSnapshot(u64);
90
91impl TickingReadinessSnapshot {
92 #[must_use]
93 pub(crate) const fn readiness(self) -> TickingReadiness {
94 match self.0 & 0b11 {
95 0 => TickingReadiness::Unready,
96 1 => TickingReadiness::BlockTicking,
97 2 => TickingReadiness::EntityTicking,
98 _ => unreachable!(),
99 }
100 }
101
102 #[must_use]
103 pub(crate) const fn is_block_ticking(self) -> bool {
104 matches!(
105 self.readiness(),
106 TickingReadiness::BlockTicking | TickingReadiness::EntityTicking
107 )
108 }
109
110 #[must_use]
111 pub(crate) const fn is_entity_ticking(self) -> bool {
112 matches!(self.readiness(), TickingReadiness::EntityTicking)
113 }
114}
115
116#[derive(Debug, Clone, Copy, PartialEq, Eq)]
117pub(crate) enum PostProcessGenerationError {
118 ChunkNotFull,
119 WorldUnavailable,
120}
121
122#[derive(Debug, Default)]
123struct ChangedLightSectionSets {
124 sky: FxHashSet<SectionPos>,
125 block: FxHashSet<SectionPos>,
126}
127
128#[derive(Debug, Default, PartialEq, Eq)]
130pub struct ChangedLightSections {
131 pub sky: Vec<SectionPos>,
133 pub block: Vec<SectionPos>,
135}
136
137impl ChangedLightSections {
138 #[must_use]
140 pub const fn is_empty(&self) -> bool {
141 self.sky.is_empty() && self.block.is_empty()
142 }
143}
144
145pub struct ChunkHolder {
152 data: OnceLock<Chunk>,
153 published_status: AtomicU8,
154 status_changed: Notify,
155 generation_task: SyncMutex<Option<Arc<ChunkGenerationTask>>>,
156 generation_task_target: AtomicU8,
157 pos: ChunkPos,
158 load_level: AtomicU8,
160 simulation_level: AtomicU8,
162 started_work: AtomicUsize,
164 active_save_dependencies: AtomicUsize,
166 save_lifecycle: AtomicU8,
168 highest_allowed_status: AtomicU8,
170 min_y: i32,
172 height: i32,
174 has_changed_sections: AtomicBool,
176 queued_for_broadcast: AtomicBool,
178 packet_content_revision: AtomicU64,
180 ticking_readiness: AtomicU64,
182 full_status_initialized: AtomicBool,
184 full_publications: Weak<FullPublicationQueue>,
186 changed_blocks_per_section: Box<[SyncMutex<FxHashSet<PackedSectionBlockPos>>]>,
189 changed_light_sections: SyncMutex<ChangedLightSectionSets>,
191}
192
193struct StatusWorkClaim {
194 holder: Arc<ChunkHolder>,
195 status: ChunkStatus,
196}
197
198impl StatusWorkClaim {
199 const fn new(holder: Arc<ChunkHolder>, status: ChunkStatus) -> Self {
200 Self { holder, status }
201 }
202}
203
204impl Drop for StatusWorkClaim {
205 fn drop(&mut self) {
206 self.holder.release_status_work_claim(self.status);
207 }
208}
209
210pub(crate) struct ChunkSaveDependency {
211 holder: Arc<ChunkHolder>,
212}
213
214impl Drop for ChunkSaveDependency {
215 fn drop(&mut self) {
216 self.holder
217 .active_save_dependencies
218 .fetch_sub(1, Ordering::AcqRel);
219 }
220}
221
222pub(crate) struct ChunkSavePreparationGuard {
227 holder: Arc<ChunkHolder>,
228 released: bool,
230}
231
232impl ChunkSavePreparationGuard {
233 #[must_use = "a snapshot prepared while losing a revival race must be discarded"]
235 pub(crate) fn finish<T>(mut self, value: T) -> Option<T> {
236 self.released = true;
237 self.holder.end_save_preparation().then_some(value)
238 }
239}
240
241impl Drop for ChunkSavePreparationGuard {
242 fn drop(&mut self) {
243 if self.released {
244 return;
245 }
246
247 let _ = self.holder.end_save_preparation();
249 }
250}
251
252impl ChunkHolder {
253 pub const fn get_pos(&self) -> ChunkPos {
255 self.pos
256 }
257
258 pub const fn min_y(&self) -> i32 {
260 self.min_y
261 }
262
263 pub const fn height(&self) -> i32 {
265 self.height
266 }
267
268 #[must_use]
270 pub fn new(
271 pos: ChunkPos,
272 load_level: ChunkTicketLevel,
273 simulation_level: Option<ChunkTicketLevel>,
274 min_y: i32,
275 height: i32,
276 ) -> Self {
277 Self::new_with_full_publications(
278 pos,
279 load_level,
280 simulation_level,
281 min_y,
282 height,
283 Weak::new(),
284 )
285 }
286
287 pub(crate) fn new_with_full_publications(
288 pos: ChunkPos,
289 load_level: ChunkTicketLevel,
290 simulation_level: Option<ChunkTicketLevel>,
291 min_y: i32,
292 height: i32,
293 full_publications: Weak<FullPublicationQueue>,
294 ) -> Self {
295 let highest_allowed_status =
296 generation_status(Some(load_level)).map_or(STATUS_NONE, |s| s.get_index() as u8);
297
298 let section_count = (height / 16) as usize;
299 let changed_blocks_per_section = (0..section_count)
300 .map(|_| SyncMutex::new(FxHashSet::default()))
301 .collect::<Box<[_]>>();
302
303 Self {
304 data: OnceLock::new(),
305 published_status: AtomicU8::new(UNPUBLISHED_STATUS),
306 status_changed: Notify::new(),
307 generation_task: SyncMutex::new(None),
308 generation_task_target: AtomicU8::new(STATUS_NONE),
309 pos,
310 load_level: AtomicU8::new(load_level.raw()),
311 simulation_level: AtomicU8::new(optional_ticket_level_raw(simulation_level)),
312 started_work: AtomicUsize::new(usize::MAX),
313 active_save_dependencies: AtomicUsize::new(0),
314 save_lifecycle: AtomicU8::new(SAVE_LIFECYCLE_ACTIVE),
315 highest_allowed_status: AtomicU8::new(highest_allowed_status),
316 min_y,
317 height,
318 has_changed_sections: AtomicBool::new(false),
319 queued_for_broadcast: AtomicBool::new(false),
320 packet_content_revision: AtomicU64::new(0),
321 ticking_readiness: AtomicU64::new(0),
322 full_status_initialized: AtomicBool::new(false),
323 full_publications,
324 changed_blocks_per_section,
325 changed_light_sections: SyncMutex::new(ChangedLightSectionSets::default()),
326 }
327 }
328
329 pub fn load_level(&self) -> Option<ChunkTicketLevel> {
331 optional_ticket_level_from_raw(self.load_level.load(Ordering::Relaxed))
332 }
333
334 pub(crate) fn swap_load_level(&self, level: ChunkTicketLevel) -> Option<ChunkTicketLevel> {
336 optional_ticket_level_from_raw(self.load_level.swap(level.raw(), Ordering::Relaxed))
337 }
338
339 pub(crate) fn clear_load_level(&self) {
341 self.load_level.store(NO_TICKET_LEVEL, Ordering::Relaxed);
342 }
343
344 pub fn simulation_level(&self) -> Option<ChunkTicketLevel> {
346 optional_ticket_level_from_raw(self.simulation_level.load(Ordering::Relaxed))
347 }
348
349 pub(crate) fn set_simulation_level(&self, level: Option<ChunkTicketLevel>) {
351 self.simulation_level
352 .store(optional_ticket_level_raw(level), Ordering::Relaxed);
353 }
354
355 pub(crate) fn entity_visibility(&self) -> EntityVisibility {
356 if self.try_chunk(ChunkStatus::Full).is_none() {
357 return EntityVisibility::Hidden;
358 }
359
360 if !self.load_level().is_some_and(is_full) {
361 return EntityVisibility::Hidden;
362 }
363
364 if is_entity_ticking(self.simulation_level())
365 && self.ticking_readiness_snapshot().is_entity_ticking()
366 {
367 EntityVisibility::Ticking
368 } else {
369 EntityVisibility::Tracked
370 }
371 }
372
373 pub fn update_highest_allowed_status(&self, ticket_level: Option<ChunkTicketLevel>) {
375 let new_status =
376 generation_status(ticket_level).map_or(STATUS_NONE, |s| s.get_index() as u8);
377 self.highest_allowed_status
378 .store(new_status, Ordering::Release);
379 }
380
381 pub fn block_changed(&self, pos: BlockPos) -> bool {
384 if !self.ticking_readiness_snapshot().is_block_ticking()
385 || pos.0.y < self.min_y
386 || pos.0.y >= self.min_y + self.height
387 {
388 return false;
389 }
390
391 let section_index = ((pos.0.y - self.min_y) / 16) as usize;
392 if section_index >= self.changed_blocks_per_section.len() {
393 return false;
394 }
395
396 let packed = SectionPos::section_relative_pos(pos);
397 self.changed_blocks_per_section[section_index]
398 .lock()
399 .insert(packed);
400 self.mark_packet_content_changed();
401 self.has_changed_sections.store(true, Ordering::Release);
402
403 !self.queued_for_broadcast.swap(true, Ordering::AcqRel)
404 }
405
406 pub fn light_changed(&self, layer: LightLayer, section_pos: SectionPos) -> bool {
410 let Some(ready_for_packet) = self.mark_valid_light_section_dirty(section_pos) else {
411 return false;
412 };
413 if !ready_for_packet {
414 return false;
415 }
416 self.mark_packet_content_changed();
417
418 let inserted = {
419 let mut guard = self.changed_light_sections.lock();
420 match layer {
421 LightLayer::Sky => guard.sky.insert(section_pos),
422 LightLayer::Block => guard.block.insert(section_pos),
423 }
424 };
425
426 if !inserted {
427 return false;
428 }
429
430 !self.queued_for_broadcast.swap(true, Ordering::AcqRel)
431 }
432
433 pub fn mark_light_section_dirty(&self, section_pos: SectionPos) -> bool {
435 self.mark_valid_light_section_dirty(section_pos).is_some()
436 }
437
438 fn mark_valid_light_section_dirty(&self, section_pos: SectionPos) -> Option<bool> {
439 if section_pos.x() != self.pos.0.x || section_pos.z() != self.pos.0.y {
440 return None;
441 }
442
443 let Ok(range) = LightSectionRange::from_world_height(self.min_y, self.height) else {
444 return None;
445 };
446 range.section_index(section_pos.y())?;
447
448 let status = self.published_status()?;
449 let chunk = self.data.get()?;
450 chunk.mark_dirty();
451 Some(status == ChunkStatus::Full && self.ticking_readiness_snapshot().is_block_ticking())
452 }
453
454 pub fn has_changes_to_broadcast(&self) -> bool {
456 self.queued_for_broadcast.load(Ordering::Acquire)
457 }
458
459 pub fn clear_broadcast_queued(&self) {
461 self.queued_for_broadcast.store(false, Ordering::Release);
462 }
463
464 pub fn take_changed_blocks(&self) -> Vec<(usize, FxHashSet<PackedSectionBlockPos>)> {
467 if !self.has_changed_sections.swap(false, Ordering::AcqRel) {
468 return Vec::new();
469 }
470
471 let mut result = Vec::new();
472 for (section_index, section_changes) in self.changed_blocks_per_section.iter().enumerate() {
473 let mut guard = section_changes.lock();
474 if !guard.is_empty() {
475 result.push((section_index, mem::take(&mut *guard)));
476 }
477 }
478 result
479 }
480
481 pub fn take_changed_light_sections(&self) -> ChangedLightSections {
483 let mut guard = self.changed_light_sections.lock();
484 ChangedLightSections {
485 sky: guard.sky.drain().collect(),
486 block: guard.block.drain().collect(),
487 }
488 }
489
490 pub fn mark_packet_content_changed(&self) {
492 self.packet_content_revision.fetch_add(1, Ordering::AcqRel);
493 }
494
495 pub fn packet_content_revision(&self) -> u64 {
497 self.packet_content_revision.load(Ordering::Acquire)
498 }
499
500 #[must_use]
501 pub(crate) fn ticking_readiness_snapshot(&self) -> TickingReadinessSnapshot {
502 TickingReadinessSnapshot(self.ticking_readiness.load(Ordering::Acquire))
503 }
504
505 #[must_use]
506 pub(crate) fn is_full_status_initialized(&self) -> bool {
507 self.full_status_initialized.load(Ordering::Acquire)
508 }
509
510 pub(crate) fn transition_ticking_readiness(
511 &self,
512 target: TickingReadiness,
513 ) -> Option<TickingReadiness> {
514 let mut current = self.ticking_readiness.load(Ordering::Acquire);
515 loop {
516 let snapshot = TickingReadinessSnapshot(current);
517 let previous = snapshot.readiness();
518 if previous == target {
519 return None;
520 }
521
522 let generation = current >> 2;
523 assert!(
524 generation != u64::MAX >> 2,
525 "chunk ticking readiness generation exhausted"
526 );
527 let next_generation = generation + 1;
528 let next = (next_generation << 2) | target as u64;
529 match self.ticking_readiness.compare_exchange(
530 current,
531 next,
532 Ordering::AcqRel,
533 Ordering::Acquire,
534 ) {
535 Ok(_) => return Some(previous),
536 Err(observed) => current = observed,
537 }
538 }
539 }
540
541 pub fn section_count(&self) -> usize {
543 self.changed_blocks_per_section.len()
544 }
545
546 pub fn is_status_disallowed(&self, status: ChunkStatus) -> bool {
548 let allowed = self.highest_allowed_status.load(Ordering::Acquire);
549 if allowed == STATUS_NONE {
550 return true;
551 }
552 status.get_index() > allowed as usize
553 }
554
555 #[inline]
560 pub(crate) fn schedule_chunk_generation_task_b(
561 &self,
562 status: ChunkStatus,
563 chunk_map: &Arc<ChunkMap>,
564 ) -> bool {
565 if self.is_status_disallowed(status) {
566 return false;
567 }
568
569 if self.try_chunk(status).is_some() {
570 return false;
571 }
572
573 let status_index = status.get_index() as u8;
574 let current_target = self.generation_task_target.load(Ordering::Acquire);
575 if current_target != STATUS_NONE && status_index <= current_target {
576 return false;
577 }
578
579 let task = self.generation_task.lock();
580
581 if task
582 .as_ref()
583 .is_some_and(|task| status <= task.target_status)
584 {
585 return false;
586 }
587
588 drop(task);
589 self.reschedule_chunk_task_b(status, chunk_map);
590 true
591 }
592
593 #[inline]
595 pub(crate) fn reschedule_chunk_task_b(&self, status: ChunkStatus, chunk_map: &Arc<ChunkMap>) {
596 let new_task = chunk_map.schedule_generation_task_b(status, self.pos);
597 let mut old_task_guard = self.generation_task.lock();
598
599 let old_task = old_task_guard.replace(new_task);
600 self.generation_task_target
601 .store(status.get_index() as u8, Ordering::Release);
602 drop(old_task_guard);
603
604 if let Some(old_task) = old_task {
605 old_task.cancel();
606 }
607
608 chunk_map.notify_generation_refill();
609 }
610
611 #[inline]
613 pub fn try_chunk(&self, status: ChunkStatus) -> Option<&Chunk> {
614 let published = self.published_status.load(Ordering::Acquire);
615 (published >= encoded_published_status(status))
616 .then(|| self.data.get())
617 .flatten()
618 }
619
620 #[must_use]
622 pub fn try_full_chunk(&self) -> Option<FullChunkRef<'_>> {
623 self.try_chunk(ChunkStatus::Full)
624 .map(FullChunkRef::from_full_context)
625 }
626
627 pub async fn await_chunk(&self, status: ChunkStatus) -> Option<&Chunk> {
629 loop {
630 let notified = self.status_changed.notified();
633
634 if self.published_status.load(Ordering::Acquire) >= encoded_published_status(status) {
635 return self.data.get();
636 }
637
638 if self.is_status_disallowed(status) {
639 return None;
640 }
641
642 notified.await;
643 }
644 }
645
646 pub async fn await_chunk_status(&self, status: ChunkStatus) -> Option<ChunkStatus> {
648 loop {
649 let notified = self.status_changed.notified();
650 let published = self.published_status();
651 if published.is_some_and(|current| status <= current) {
652 return published;
653 }
654
655 if self.is_status_disallowed(status) {
656 return None;
657 }
658
659 notified.await;
660 }
661 }
662
663 async fn await_claimed_chunk_status(&self, status: ChunkStatus) -> Option<ChunkStatus> {
664 loop {
665 let notified = self.status_changed.notified();
666 let published = self.published_status();
667 if published.is_some_and(|current| status <= current) {
668 return published;
669 }
670
671 if self.is_status_disallowed(status) || !self.status_work_covers(status) {
672 return None;
673 }
674
675 notified.await;
676 }
677 }
678
679 pub fn published_status(&self) -> Option<ChunkStatus> {
681 decoded_published_status(self.published_status.load(Ordering::Acquire))
682 }
683
684 #[must_use]
686 pub fn is_ready_for_saving(&self) -> bool {
687 self.active_save_dependencies.load(Ordering::Acquire) == 0
688 }
689
690 pub(crate) fn add_save_dependency(self: &Arc<Self>) -> ChunkSaveDependency {
691 self.active_save_dependencies.fetch_add(1, Ordering::AcqRel);
692 ChunkSaveDependency {
693 holder: Arc::clone(self),
694 }
695 }
696
697 pub(crate) fn begin_unloading(&self) {
699 let result = self.save_lifecycle.compare_exchange(
700 SAVE_LIFECYCLE_ACTIVE,
701 SAVE_LIFECYCLE_UNLOADING,
702 Ordering::AcqRel,
703 Ordering::Acquire,
704 );
705 assert!(
706 result.is_ok(),
707 "an active chunk holder entered unloading from an invalid lifecycle"
708 );
709 }
710
711 pub(crate) fn try_begin_save_preparation(
713 self: &Arc<Self>,
714 ) -> Option<ChunkSavePreparationGuard> {
715 self.save_lifecycle
716 .compare_exchange(
717 SAVE_LIFECYCLE_UNLOADING,
718 SAVE_LIFECYCLE_PREPARING,
719 Ordering::AcqRel,
720 Ordering::Acquire,
721 )
722 .ok()
723 .map(|_| ChunkSavePreparationGuard {
724 holder: Arc::clone(self),
725 released: false,
726 })
727 }
728
729 fn end_save_preparation(&self) -> bool {
731 self.save_lifecycle
732 .compare_exchange(
733 SAVE_LIFECYCLE_PREPARING,
734 SAVE_LIFECYCLE_UNLOADING,
735 Ordering::AcqRel,
736 Ordering::Acquire,
737 )
738 .is_ok()
739 }
740
741 pub(crate) fn revive_from_unloading(&self) {
745 let previous =
746 self.save_lifecycle
747 .try_update(Ordering::AcqRel, Ordering::Acquire, |lifecycle| {
748 (lifecycle != SAVE_LIFECYCLE_ACTIVE).then_some(SAVE_LIFECYCLE_ACTIVE)
749 });
750 debug_assert!(
751 previous.is_ok(),
752 "an already active chunk holder was revived from unloading"
753 );
754 }
755
756 pub fn apply_step(
768 self: &Arc<Self>,
769 step: &'static ChunkStep,
770 chunk_map: &Arc<ChunkMap>,
771 cache: &Arc<StaticCache2D<Arc<ChunkHolder>>>,
772 thread_pool: Arc<rayon::ThreadPool>,
773 ) -> Option<NeighborReady> {
774 let target_status = step.target_status;
775
776 if self.is_status_disallowed(target_status) {
777 return None;
778 }
779
780 if target_status == ChunkStatus::Light {
781 let light_work_window_gate = chunk_map.light_work_window_gate();
782 let Some(light_work_window_reservation) =
783 light_work_window_gate.try_reserve_centered(self.pos)
784 else {
785 return Some(Self::await_light_work_window_and_apply_step(
786 Arc::clone(self),
787 step,
788 Arc::clone(chunk_map),
789 Arc::clone(cache),
790 thread_pool,
791 light_work_window_gate,
792 ));
793 };
794
795 return self.apply_step_with_light_work_window_reservation(
796 step,
797 chunk_map,
798 cache,
799 thread_pool,
800 Some(light_work_window_reservation),
801 );
802 }
803
804 self.apply_step_with_light_work_window_reservation(
805 step,
806 chunk_map,
807 cache,
808 thread_pool,
809 None,
810 )
811 }
812
813 fn await_light_work_window_and_apply_step(
814 holder: Arc<Self>,
815 step: &'static ChunkStep,
816 chunk_map: Arc<ChunkMap>,
817 cache: Arc<StaticCache2D<Arc<ChunkHolder>>>,
818 thread_pool: Arc<rayon::ThreadPool>,
819 light_work_window_gate: Arc<LightWorkWindowGate>,
820 ) -> NeighborReady {
821 Box::pin(async move {
822 let light_work_window_reservation =
823 light_work_window_gate.reserve_centered(holder.pos).await;
824 let ready = holder.apply_step_with_light_work_window_reservation(
825 step,
826 &chunk_map,
827 &cache,
828 thread_pool,
829 Some(light_work_window_reservation),
830 )?;
831 ready.await
832 })
833 }
834
835 fn apply_step_with_light_work_window_reservation(
836 self: &Arc<Self>,
837 step: &'static ChunkStep,
838 chunk_map: &Arc<ChunkMap>,
839 cache: &Arc<StaticCache2D<Arc<ChunkHolder>>>,
840 thread_pool: Arc<rayon::ThreadPool>,
841 light_work_window_reservation: Option<LightWorkWindowReservation>,
842 ) -> Option<NeighborReady> {
843 let target_status = step.target_status;
844 debug_assert!(
845 target_status != ChunkStatus::Light || light_work_window_reservation.is_some()
846 );
847
848 if self.is_status_disallowed(target_status) {
849 return None;
850 }
851
852 let Some(status_claim) = self.claim_status_work(target_status) else {
853 let self_clone = Arc::clone(self);
858 return Some(Box::pin(async move {
859 self_clone
860 .await_claimed_chunk_status(target_status)
861 .await
862 .map(|_| ())
863 }));
864 };
865
866 let cache = Arc::clone(cache);
867 let context = Arc::clone(&chunk_map.world_gen_context);
868 let self_clone = Arc::clone(self);
869 let storage = Arc::clone(&chunk_map.storage);
870 let save_dependency = self.add_save_dependency();
871
872 let future = chunk_map.task_tracker.spawn(async move {
873 let _status_claim = status_claim;
875 let _save_dependency = save_dependency;
876 let result = if target_status == ChunkStatus::Empty {
877 Self::apply_empty_step(self_clone, step, context, cache, storage, thread_pool).await
878 } else {
879 Self::apply_generated_step(
880 self_clone,
881 step,
882 context,
883 cache,
884 thread_pool,
885 light_work_window_reservation,
886 )
887 .await
888 };
889
890 #[cfg(feature = "slow_chunk_gen")]
891 if result.is_some() && SLOW_CHUNK_GEN.load(Ordering::Relaxed) {
892 sleep(Duration::from_millis(200)).await;
893 }
894
895 result
896 });
897
898 Some(Box::pin(async move {
899 match future.await {
900 Ok(result) => result,
901 Err(e) => {
902 log::error!("Chunk generation task panicked: {e}");
903 None
904 }
905 }
906 }))
907 }
908
909 async fn apply_empty_step(
910 holder: Arc<Self>,
911 step: &'static ChunkStep,
912 context: Arc<WorldGenContext>,
913 cache: Arc<StaticCache2D<Arc<ChunkHolder>>>,
914 storage: Arc<ChunkStorage>,
915 thread_pool: Arc<rayon::ThreadPool>,
916 ) -> Option<()> {
917 let target_status = step.target_status;
918 let chunk_exists = match storage.acquire_chunk(holder.pos).await {
919 Ok(chunk_exists) => chunk_exists,
920 Err(error) => {
921 tracing::error!(
922 chunk = ?holder.pos,
923 "Failed to acquire chunk storage before load/generation: {error}",
924 );
925 return None;
926 }
927 };
928
929 if holder.is_status_disallowed(target_status) {
930 tracing::debug!(
931 chunk = ?holder.pos,
932 ?target_status,
933 load_level = ?holder.load_level(),
934 simulation_level = ?holder.simulation_level(),
935 current_status = ?holder.published_status(),
936 "Dropping storage load after chunk holder target became disallowed before load/generation: chunk={:?}, target_status={:?}, load_level={:?}, simulation_level={:?}, current_status={:?}",
937 holder.pos,
938 target_status,
939 holder.load_level(),
940 holder.simulation_level(),
941 holder.published_status(),
942 );
943 if let Err(error) = storage.release_chunk(holder.pos).await {
944 tracing::error!(
945 chunk = ?holder.pos,
946 "Failed to release canceled chunk storage task: {error}",
947 );
948 }
949 return None;
950 }
951
952 if chunk_exists {
953 match Self::apply_existing_empty_step(
954 &holder,
955 target_status,
956 &context,
957 &storage,
958 &thread_pool,
959 )
960 .await
961 {
962 Some(true) => return Some(()),
963 Some(false) => {}
964 None => return None,
965 }
966 }
967
968 if holder.is_status_disallowed(target_status) {
969 tracing::debug!(
970 chunk = ?holder.pos,
971 ?target_status,
972 load_level = ?holder.load_level(),
973 simulation_level = ?holder.simulation_level(),
974 current_status = ?holder.published_status(),
975 "Dropping storage load after chunk holder target became disallowed after load attempt: chunk={:?}, target_status={:?}, load_level={:?}, simulation_level={:?}, current_status={:?}",
976 holder.pos,
977 target_status,
978 holder.load_level(),
979 holder.simulation_level(),
980 holder.published_status(),
981 );
982 if let Err(error) = storage.release_chunk(holder.pos).await {
983 tracing::error!(
984 chunk = ?holder.pos,
985 "Failed to release canceled chunk storage task: {error}",
986 );
987 }
988 return None;
989 }
990
991 let holder_for_notify = Arc::clone(&holder);
992 let world = context.world();
993 Self::run_step_task(thread_pool, step, context, cache, holder).await;
994 holder_for_notify.finish_generation_status(target_status);
995 if target_status == ChunkStatus::Empty {
996 world.on_entity_chunk_loaded(holder_for_notify.pos);
997 }
998 Some(())
999 }
1000
1001 async fn apply_existing_empty_step(
1002 holder: &Arc<Self>,
1003 target_status: ChunkStatus,
1004 context: &Arc<WorldGenContext>,
1005 storage: &Arc<ChunkStorage>,
1006 thread_pool: &rayon::ThreadPool,
1007 ) -> Option<bool> {
1008 let loaded = match storage
1009 .load_chunk(
1010 holder.pos,
1011 holder.min_y(),
1012 holder.height(),
1013 context.weak_world(),
1014 thread_pool,
1015 )
1016 .await
1017 {
1018 Ok(Some(loaded)) => loaded,
1019 Ok(None) => {
1020 tracing::warn!(
1021 chunk = ?holder.pos,
1022 "Chunk storage entry disappeared or was discarded as corrupt; regenerating it",
1023 );
1024 return Some(false);
1025 }
1026 Err(error) => {
1027 tracing::error!(
1028 chunk = ?holder.pos,
1029 "Failed to load existing chunk; aborting generation to avoid overwriting saved data: {error}",
1030 );
1031 if let Err(release_error) = storage.release_chunk(holder.pos).await {
1032 tracing::error!(
1033 chunk = ?holder.pos,
1034 "Failed to release chunk storage after load failure: {release_error}",
1035 );
1036 }
1037 return None;
1038 }
1039 };
1040
1041 let loaded_status = loaded.status;
1042 if holder.is_status_disallowed(target_status) {
1043 tracing::debug!(
1044 chunk = ?holder.pos,
1045 ?target_status,
1046 ?loaded_status,
1047 load_level = ?holder.load_level(),
1048 simulation_level = ?holder.simulation_level(),
1049 current_status = ?holder.published_status(),
1050 "Dropping storage load that completed after chunk holder target became disallowed: chunk={:?}, target_status={:?}, loaded_status={:?}, load_level={:?}, simulation_level={:?}, current_status={:?}",
1051 holder.pos,
1052 target_status,
1053 loaded_status,
1054 holder.load_level(),
1055 holder.simulation_level(),
1056 holder.published_status(),
1057 );
1058 if let Err(error) = storage.release_chunk(holder.pos).await {
1059 tracing::error!(
1060 chunk = ?holder.pos,
1061 "Failed to release canceled chunk storage load: {error}",
1062 );
1063 }
1064 return None;
1065 }
1066
1067 holder.store_and_publish_chunk_status(loaded.chunk, loaded_status);
1068 let world = context.world();
1069 world.on_entity_chunk_loaded(holder.pos);
1070 world.update_entity_chunk_visibility(holder.pos, holder.entity_visibility());
1071 if !loaded.pending_entities.is_empty() {
1072 world.register_loaded_chunk_entities(
1073 holder.pos,
1074 loaded_status,
1075 loaded.pending_entities,
1076 );
1077 }
1078 if loaded_status == ChunkStatus::Full {
1079 holder.publish_full();
1080 }
1081 Some(true)
1082 }
1083
1084 async fn apply_generated_step(
1085 holder: Arc<Self>,
1086 step: &'static ChunkStep,
1087 context: Arc<WorldGenContext>,
1088 cache: Arc<StaticCache2D<Arc<ChunkHolder>>>,
1089 thread_pool: Arc<rayon::ThreadPool>,
1090 light_work_window_reservation: Option<LightWorkWindowReservation>,
1091 ) -> Option<()> {
1092 let target_status = step.target_status;
1093 let Some(parent_status) = target_status.parent() else {
1094 panic!("Target status must have parent if not Empty");
1095 };
1096 let has_parent = holder
1097 .published_status()
1098 .is_some_and(|status| parent_status <= status);
1099 let holder_for_notify = Arc::clone(&holder);
1100
1101 assert!(has_parent, "Parent chunk missing");
1102
1103 Self::run_step_task(thread_pool, step, context, cache, holder).await;
1104 holder_for_notify.finish_generation_status(target_status);
1105 drop(light_work_window_reservation);
1106 Some(())
1107 }
1108
1109 async fn run_step_task(
1110 thread_pool: Arc<rayon::ThreadPool>,
1111 step: &'static ChunkStep,
1112 context: Arc<WorldGenContext>,
1113 cache: Arc<StaticCache2D<Arc<ChunkHolder>>>,
1114 holder: Arc<Self>,
1115 ) {
1116 let task = step.task;
1117 rayon_spawn(&thread_pool, move || {
1118 task(context, step, &cache, holder);
1119 })
1120 .await;
1121 }
1122
1123 fn claim_status_work(self: &Arc<Self>, status: ChunkStatus) -> Option<StatusWorkClaim> {
1124 let status_index = status.get_index();
1125 let parent_index = status.parent().map_or(usize::MAX, ChunkStatus::get_index);
1126
1127 let previous_started = self.started_work.compare_exchange(
1128 parent_index,
1129 status_index,
1130 Ordering::SeqCst,
1131 Ordering::SeqCst,
1132 );
1133
1134 match previous_started {
1135 Ok(_) => Some(StatusWorkClaim::new(Arc::clone(self), status)),
1136 Err(current) => {
1137 if current != usize::MAX && current >= status_index {
1138 None
1139 } else {
1140 panic!(
1141 "Unexpected started work status: {current:?} (index {current}) while trying to start: {status:?} (index {status_index})"
1142 );
1143 }
1144 }
1145 }
1146 }
1147
1148 fn release_status_work_claim(&self, status: ChunkStatus) {
1149 let status_index = status.get_index();
1150 let rollback_index = self
1151 .published_status()
1152 .map_or(usize::MAX, ChunkStatus::get_index);
1153
1154 if rollback_index != usize::MAX && rollback_index >= status_index {
1155 return;
1156 }
1157
1158 if self
1159 .started_work
1160 .compare_exchange(
1161 status_index,
1162 rollback_index,
1163 Ordering::SeqCst,
1164 Ordering::SeqCst,
1165 )
1166 .is_ok()
1167 {
1168 self.wake_all_watchers();
1169 }
1170 }
1171
1172 fn mark_status_work_published(&self, status: ChunkStatus) {
1173 let status_index = status.get_index();
1174 let mut current = self.started_work.load(Ordering::Acquire);
1175
1176 loop {
1177 if current != usize::MAX && current >= status_index {
1178 return;
1179 }
1180
1181 match self.started_work.compare_exchange(
1182 current,
1183 status_index,
1184 Ordering::SeqCst,
1185 Ordering::SeqCst,
1186 ) {
1187 Ok(_) => return,
1188 Err(next) => current = next,
1189 }
1190 }
1191 }
1192
1193 fn status_work_covers(&self, status: ChunkStatus) -> bool {
1194 let current = self.started_work.load(Ordering::Acquire);
1195 current != usize::MAX && current >= status.get_index()
1196 }
1197
1198 pub(crate) fn upgrade_to_full(&self) {
1205 if self.published_status() == Some(ChunkStatus::Full) {
1206 return;
1207 }
1208 let Some(chunk) = self.data.get() else {
1209 panic!("cannot promote an uninitialized chunk holder");
1210 };
1211 let FullChunkPromotion {
1212 chunk: full,
1213 pending_entities,
1214 } = chunk.promote_to_full();
1215 let promoted_entities = Some((full.get_level(), chunk.pos, pending_entities));
1216 if let Some((world, pos, pending_entities)) = promoted_entities
1217 && let Some(world) = world
1218 {
1219 world.register_loaded_chunk_entities(pos, ChunkStatus::Full, pending_entities);
1220 }
1221 }
1222
1223 pub(crate) fn post_process_generation(&self) -> Result<usize, PostProcessGenerationError> {
1225 let postprocessing = {
1226 let Some(full) = self.try_full_chunk() else {
1227 return Err(PostProcessGenerationError::ChunkNotFull);
1228 };
1229 let world = full
1230 .get_level()
1231 .ok_or(PostProcessGenerationError::WorldUnavailable)?;
1232 full.take_postprocessing()
1233 .map(|postprocessing| (world, full.common().pos, full.min_y(), postprocessing))
1234 };
1235
1236 let post_process_position_count =
1237 if let Some((world, pos, min_y, postprocessing)) = postprocessing {
1238 let position_count = postprocessing.iter().map(Vec::len).sum();
1239 FullChunkRef::post_process_generation(&world, pos, min_y, postprocessing);
1240 position_count
1241 } else {
1242 0
1243 };
1244 let Some(full) = self.try_full_chunk() else {
1245 return Err(PostProcessGenerationError::ChunkNotFull);
1246 };
1247 full.promote_pending_block_entities();
1248 Ok(post_process_position_count)
1249 }
1250
1251 fn finish_generation_status(self: &Arc<Self>, status: ChunkStatus) {
1253 if let Some(stored_chunk) = self.data.get()
1254 && self
1255 .published_status()
1256 .is_none_or(|published| published < status)
1257 {
1258 stored_chunk.mark_dirty();
1259 }
1260
1261 if status == ChunkStatus::Full {
1262 self.register_full_chunk_ticks();
1263 }
1264
1265 self.mark_status_work_published(status);
1266 self.publish_generated_status(status);
1267
1268 if status == ChunkStatus::Full {
1269 self.publish_full();
1270 }
1271 }
1272
1273 #[cfg(test)]
1274 pub(crate) fn finish_generation_status_for_test(self: &Arc<Self>, status: ChunkStatus) {
1275 self.finish_generation_status(status);
1276 }
1277
1278 pub fn insert_chunk(self: &Arc<Self>, chunk: Chunk, status: ChunkStatus) {
1287 self.store_and_publish_chunk_status(chunk, status);
1288 if status == ChunkStatus::Full {
1289 self.publish_full();
1290 }
1291 }
1292
1293 fn store_and_publish_chunk_status(&self, chunk: Chunk, status: ChunkStatus) {
1294 assert_eq!(
1295 self.published_status.load(Ordering::Acquire),
1296 UNPUBLISHED_STATUS,
1297 "initial chunk installation cannot replace published data"
1298 );
1299 assert_eq!(
1300 status == ChunkStatus::Full,
1301 chunk.full_runtime().is_some(),
1302 "initial chunk status must match its Full runtime state"
1303 );
1304 assert!(
1305 self.data.set(chunk).is_ok(),
1306 "initial chunk installation cannot replace existing data"
1307 );
1308 if status == ChunkStatus::Full {
1309 self.register_full_chunk_ticks();
1310 }
1311 self.mark_status_work_published(status);
1312 self.published_status
1313 .store(encoded_published_status(status), Ordering::Release);
1314 self.status_changed.notify_waiters();
1315 }
1316
1317 fn publish_generated_status(&self, status: ChunkStatus) {
1318 let encoded = encoded_published_status(status);
1319 let previous = self.published_status.fetch_max(encoded, Ordering::Release);
1320 if previous < encoded {
1321 self.status_changed.notify_waiters();
1322 }
1323 }
1324
1325 fn register_full_chunk_ticks(&self) {
1327 let Some(chunk) = self.data.get() else {
1328 panic!("Full status must have installed chunk data");
1329 };
1330 let Some(_) = chunk.full_runtime() else {
1331 panic!("Full status must expose a Full chunk view");
1332 };
1333 let full = FullChunkRef::from_full_context(chunk);
1334 let Some(world) = full.get_level() else {
1335 return;
1338 };
1339 if let Err(error) = world.register_full_chunk_ticks(full) {
1340 panic!("Full chunk scheduled-tick registration invariant failed: {error:?}");
1341 }
1342 }
1343
1344 fn publish_full(self: &Arc<Self>) {
1345 let Some(full) = self.try_full_chunk() else {
1346 return;
1347 };
1348 let world = full.get_level();
1349 if let Some(world) = world {
1350 world.update_entity_chunk_visibility(self.pos, self.entity_visibility());
1351 }
1352 self.full_status_initialized.store(true, Ordering::Release);
1353 if let Some(publications) = self.full_publications.upgrade() {
1354 publications.publish(self);
1355 }
1356 }
1357
1358 pub(crate) fn insert_chunk_no_notify(&self, chunk: Chunk) {
1361 assert!(
1362 self.data.set(chunk).is_ok(),
1363 "initial chunk installation cannot replace existing data"
1364 );
1365 }
1366
1367 pub fn wake_all_watchers(&self) {
1371 self.status_changed.notify_waiters();
1372 }
1373
1374 pub fn cancel_generation_task(&self) {
1376 let mut task_guard = self.generation_task.lock();
1377 self.generation_task_target
1378 .store(STATUS_NONE, Ordering::Release);
1379 if let Some(task) = task_guard.take() {
1380 task.cancel();
1381 }
1382 }
1383
1384 pub(crate) fn clear_generation_task_if_current(&self, task: &Arc<ChunkGenerationTask>) {
1386 let mut task_guard = self.generation_task.lock();
1387 if task_guard
1388 .as_ref()
1389 .is_some_and(|current_task| Arc::ptr_eq(current_task, task))
1390 {
1391 task_guard.take();
1392 self.generation_task_target
1393 .store(STATUS_NONE, Ordering::Release);
1394 }
1395 }
1396}
1397
1398fn rayon_spawn<F, R>(thread_pool: &rayon::ThreadPool, func: F) -> impl Future<Output = R>
1399where
1400 F: FnOnce() -> R + Send + 'static,
1401 R: Send + 'static + Debug,
1402{
1403 let (sender, receiver) = oneshot::channel();
1404 thread_pool.spawn(move || {
1405 sender.send(func()).expect("Failed to send result");
1406 });
1407 async move { receiver.await.expect("Failed to receive rayon task result") }
1408}
1409
1410#[cfg(test)]
1411mod tests {
1412 use super::*;
1413 use std::{task::Poll, time::Duration as TestDuration};
1414 use tokio::time::sleep as test_sleep;
1415
1416 use crate::behavior::init_behaviors;
1417 use crate::chunk::Chunk;
1418 use crate::chunk::section::{ChunkSection, Sections};
1419 use crate::test_support::fresh_test_world;
1420 use crate::world::tick_scheduler::TickPriority;
1421 use steel_registry::{init_vanilla_registry, vanilla_blocks, vanilla_fluids};
1422
1423 fn init_chunk_test_registry() {
1424 init_vanilla_registry();
1425 init_behaviors();
1426 }
1427
1428 fn test_holder() -> Arc<ChunkHolder> {
1429 Arc::new(ChunkHolder::new(
1430 ChunkPos::new(0, 0),
1431 ChunkTicketLevel::FULL_CHUNK,
1432 Some(ChunkTicketLevel::FULL_CHUNK),
1433 0,
1434 16,
1435 ))
1436 }
1437
1438 fn test_proto_chunk(_status: ChunkStatus) -> Chunk {
1439 Chunk::new(
1440 Sections::from_owned(vec![ChunkSection::new_empty()].into_boxed_slice()),
1441 ChunkPos::new(0, 0),
1442 0,
1443 16,
1444 Weak::new(),
1445 )
1446 }
1447
1448 #[test]
1449 fn insert_chunk_publishes_the_authoritative_status() {
1450 init_chunk_test_registry();
1451 let holder = test_holder();
1452 let proto = Chunk::new(
1453 Sections::from_owned(vec![ChunkSection::new_empty()].into_boxed_slice()),
1454 ChunkPos::new(0, 0),
1455 0,
1456 16,
1457 Weak::new(),
1458 );
1459
1460 holder.insert_chunk(proto, ChunkStatus::Light);
1461
1462 let Some(chunk) = holder.try_chunk(ChunkStatus::Light) else {
1463 panic!("inserted chunk should be available at published status");
1464 };
1465 assert_eq!(holder.published_status(), Some(ChunkStatus::Light));
1466 assert!(chunk.full_runtime().is_none());
1467 }
1468
1469 #[test]
1470 #[should_panic(expected = "initial chunk status must match its Full runtime state")]
1471 fn insert_chunk_rejects_full_status_for_proto_data() {
1472 init_chunk_test_registry();
1473 test_holder().insert_chunk(test_proto_chunk(ChunkStatus::Spawn), ChunkStatus::Full);
1474 }
1475
1476 #[test]
1477 fn full_readiness_publication_waits_for_post_load_initialization() {
1478 init_chunk_test_registry();
1479 let publications = Arc::new(FullPublicationQueue::default());
1480 let holder = Arc::new(ChunkHolder::new_with_full_publications(
1481 ChunkPos::new(0, 0),
1482 ChunkTicketLevel::FULL_CHUNK,
1483 None,
1484 0,
1485 16,
1486 Arc::downgrade(&publications),
1487 ));
1488 let full = test_proto_chunk(ChunkStatus::Light);
1489 let _ = full.promote_to_full();
1490
1491 holder.store_and_publish_chunk_status(full, ChunkStatus::Full);
1492
1493 assert_eq!(holder.published_status(), Some(ChunkStatus::Full));
1494 assert!(!holder.is_full_status_initialized());
1495 assert!(publications.drain().is_empty());
1496
1497 holder.publish_full();
1498
1499 assert!(holder.is_full_status_initialized());
1500 assert_eq!(publications.drain().len(), 1);
1501 }
1502
1503 #[test]
1504 fn generated_full_status_is_accessible_when_readiness_is_published() {
1505 init_chunk_test_registry();
1506 let holder = test_holder();
1507 holder.insert_chunk(test_proto_chunk(ChunkStatus::Light), ChunkStatus::Light);
1508 holder.upgrade_to_full();
1509
1510 assert_eq!(holder.entity_visibility(), EntityVisibility::Hidden);
1511 assert!(!holder.is_full_status_initialized());
1512
1513 holder.finish_generation_status(ChunkStatus::Full);
1514
1515 assert_eq!(holder.entity_visibility(), EntityVisibility::Tracked);
1516 assert!(holder.is_full_status_initialized());
1517 }
1518
1519 #[test]
1520 fn late_lower_generation_completion_does_not_regress_published_status() {
1521 init_chunk_test_registry();
1522 let holder = test_holder();
1523 holder.insert_chunk(test_proto_chunk(ChunkStatus::Light), ChunkStatus::Light);
1524
1525 holder.finish_generation_status(ChunkStatus::Spawn);
1526 holder.finish_generation_status(ChunkStatus::Features);
1527
1528 assert_eq!(holder.published_status(), Some(ChunkStatus::Spawn));
1529 assert!(holder.try_chunk(ChunkStatus::Spawn).is_some());
1530 }
1531
1532 #[tokio::test]
1533 async fn status_waiter_observes_publication_after_subscribing() {
1534 init_chunk_test_registry();
1535 let holder = test_holder();
1536 let waiter = holder.await_chunk_status(ChunkStatus::Empty);
1537
1538 holder.insert_chunk(test_proto_chunk(ChunkStatus::Empty), ChunkStatus::Empty);
1539
1540 assert_eq!(waiter.await, Some(ChunkStatus::Empty));
1541 }
1542
1543 #[tokio::test]
1544 async fn pending_status_waiters_wake_after_publication() {
1545 init_chunk_test_registry();
1546 let holder = test_holder();
1547 let first_waiter = holder.await_chunk_status(ChunkStatus::Empty);
1548 let second_waiter = holder.await_chunk_status(ChunkStatus::Empty);
1549 tokio::pin!(first_waiter, second_waiter);
1550 assert!(matches!(futures::poll!(&mut first_waiter), Poll::Pending));
1551 assert!(matches!(futures::poll!(&mut second_waiter), Poll::Pending));
1552
1553 let publishing_holder = Arc::clone(&holder);
1554 let publish_task = tokio::spawn(async move {
1555 publishing_holder
1556 .insert_chunk(test_proto_chunk(ChunkStatus::Empty), ChunkStatus::Empty);
1557 });
1558
1559 let (first_status, second_status) = tokio::select! {
1560 biased;
1561 () = test_sleep(TestDuration::from_secs(1)) => {
1562 panic!("pending status waiters were not woken by publication");
1563 }
1564 statuses = async { tokio::join!(&mut first_waiter, &mut second_waiter) } => statuses,
1565 };
1566 assert_eq!(first_status, Some(ChunkStatus::Empty));
1567 assert_eq!(second_status, Some(ChunkStatus::Empty));
1568 assert!(publish_task.await.is_ok());
1569 }
1570
1571 #[test]
1572 fn full_registration_transfers_prepublication_block_and_fluid_ticks() {
1573 init_chunk_test_registry();
1574 let world_fixture = fresh_test_world("prepublication_tick_transfer");
1575 let world = &world_fixture.world;
1576 let chunk_pos = ChunkPos::new(0, 0);
1577 let min_y = world.get_min_y();
1578 let height = world.get_height();
1579 let sections = (0..height / 16)
1580 .map(|_| ChunkSection::new_empty())
1581 .collect::<Vec<_>>()
1582 .into_boxed_slice();
1583 let proto = Chunk::new(
1584 Sections::from_owned(sections),
1585 chunk_pos,
1586 min_y,
1587 height,
1588 Arc::downgrade(world),
1589 );
1590 let block_pos = BlockPos::new(1, min_y + 1, 1);
1591 let fluid_pos = BlockPos::new(2, min_y + 1, 2);
1592 proto.schedule_block_tick(block_pos, &vanilla_blocks::STONE, TickPriority::High);
1593 proto.schedule_fluid_tick(fluid_pos, &vanilla_fluids::WATER, TickPriority::Low);
1594
1595 let holder = Arc::new(ChunkHolder::new(
1596 chunk_pos,
1597 ChunkTicketLevel::FULL_CHUNK,
1598 Some(ChunkTicketLevel::FULL_CHUNK),
1599 min_y,
1600 height,
1601 ));
1602 let _ = world
1603 .chunk_map
1604 .chunks
1605 .insert_sync(chunk_pos, Arc::clone(&holder));
1606 holder.insert_chunk(proto, ChunkStatus::Light);
1607 holder.upgrade_to_full();
1608
1609 assert!(!world.has_registered_full_chunk_ticks(chunk_pos));
1610 holder.finish_generation_status(ChunkStatus::Full);
1611
1612 assert!(world.has_registered_full_chunk_ticks(chunk_pos));
1613 assert!(world.has_scheduled_block_tick(block_pos, &vanilla_blocks::STONE));
1614 assert!(world.has_scheduled_fluid_tick(fluid_pos, &vanilla_fluids::WATER));
1615 }
1616
1617 #[test]
1618 fn client_deltas_require_confirmed_block_readiness() {
1619 init_chunk_test_registry();
1620 let holder = test_holder();
1621 let full = test_proto_chunk(ChunkStatus::Light);
1622 let _ = full.promote_to_full();
1623 holder.insert_chunk(full, ChunkStatus::Full);
1624 let pos = BlockPos::new(1, 1, 1);
1625 let section_pos = SectionPos::new(0, 0, 0);
1626 let revision = holder.packet_content_revision();
1627 let chunk = holder
1628 .try_chunk(ChunkStatus::Full)
1629 .expect("the test holder should contain a Full chunk");
1630 chunk.clear_dirty();
1631
1632 assert!(!holder.block_changed(pos));
1633 assert!(!holder.light_changed(LightLayer::Block, section_pos));
1634 assert_eq!(holder.packet_content_revision(), revision);
1635 assert!(
1636 holder
1637 .try_chunk(ChunkStatus::Full)
1638 .is_some_and(Chunk::is_dirty),
1639 "pre-readiness light changes must still be persisted"
1640 );
1641
1642 holder.transition_ticking_readiness(TickingReadiness::BlockTicking);
1643
1644 assert!(holder.light_changed(LightLayer::Block, section_pos));
1645 assert_eq!(holder.packet_content_revision(), revision + 1);
1646 holder.clear_broadcast_queued();
1647 assert!(holder.block_changed(pos));
1648 assert_eq!(holder.packet_content_revision(), revision + 2);
1649 }
1650
1651 #[test]
1652 fn unpublished_status_claim_rolls_back_to_unloaded() {
1653 let holder = test_holder();
1654 let claim = holder
1655 .claim_status_work(ChunkStatus::Empty)
1656 .expect("empty status should be claimable");
1657
1658 assert!(holder.claim_status_work(ChunkStatus::Empty).is_none());
1659
1660 drop(claim);
1661
1662 assert!(!holder.status_work_covers(ChunkStatus::Empty));
1663 let retry = holder
1664 .claim_status_work(ChunkStatus::Empty)
1665 .expect("abandoned empty status should be claimable again");
1666 drop(retry);
1667 }
1668
1669 #[test]
1670 fn unpublished_child_claim_rolls_back_to_published_parent() {
1671 init_chunk_test_registry();
1672 let holder = test_holder();
1673 holder.insert_chunk(test_proto_chunk(ChunkStatus::Empty), ChunkStatus::Empty);
1674
1675 let claim = holder
1676 .claim_status_work(ChunkStatus::StructureStarts)
1677 .expect("child status should be claimable after parent is published");
1678
1679 drop(claim);
1680
1681 assert!(holder.status_work_covers(ChunkStatus::Empty));
1682 assert!(!holder.status_work_covers(ChunkStatus::StructureStarts));
1683 let retry = holder
1684 .claim_status_work(ChunkStatus::StructureStarts)
1685 .expect("abandoned child status should be claimable again");
1686 drop(retry);
1687 }
1688
1689 #[test]
1690 fn empty_claim_can_publish_a_higher_loaded_status() {
1691 init_chunk_test_registry();
1692 let holder = test_holder();
1693 let empty_claim = holder
1694 .claim_status_work(ChunkStatus::Empty)
1695 .expect("empty status should be claimable");
1696
1697 holder.insert_chunk(
1698 test_proto_chunk(ChunkStatus::StructureStarts),
1699 ChunkStatus::StructureStarts,
1700 );
1701 drop(empty_claim);
1702
1703 assert!(holder.status_work_covers(ChunkStatus::StructureStarts));
1704 assert!(!holder.status_work_covers(ChunkStatus::StructureReferences));
1705 let next_claim = holder
1706 .claim_status_work(ChunkStatus::StructureReferences)
1707 .expect("next status should be claimable from loaded status");
1708 drop(next_claim);
1709 }
1710
1711 #[tokio::test]
1712 async fn claimed_status_waiter_finishes_when_claim_is_abandoned() {
1713 let holder = test_holder();
1714 let claim = holder
1715 .claim_status_work(ChunkStatus::Empty)
1716 .expect("empty status should be claimable");
1717 let waiter = holder.await_claimed_chunk_status(ChunkStatus::Empty);
1718
1719 drop(claim);
1720
1721 assert!(waiter.await.is_none());
1722 }
1723
1724 #[test]
1725 fn save_dependency_controls_ready_for_saving() {
1726 let holder = test_holder();
1727 assert!(holder.is_ready_for_saving());
1728
1729 let first = holder.add_save_dependency();
1730 let second = holder.add_save_dependency();
1731 assert!(!holder.is_ready_for_saving());
1732
1733 drop(first);
1734 assert!(!holder.is_ready_for_saving());
1735
1736 drop(second);
1737 assert!(holder.is_ready_for_saving());
1738 }
1739
1740 #[test]
1741 fn revival_wins_over_an_in_flight_save_preparation() {
1742 let holder = test_holder();
1743 holder.begin_unloading();
1744 let preparation = holder
1745 .try_begin_save_preparation()
1746 .expect("an unloading holder should begin save preparation");
1747
1748 holder.revive_from_unloading();
1749
1750 assert!(
1751 preparation.finish(()).is_none(),
1752 "a preparation that lost the revival race must discard its snapshot"
1753 );
1754 assert!(holder.try_begin_save_preparation().is_none());
1755 }
1756
1757 #[test]
1758 fn an_uncontested_save_preparation_keeps_its_snapshot() {
1759 let holder = test_holder();
1760 holder.begin_unloading();
1761 let preparation = holder
1762 .try_begin_save_preparation()
1763 .expect("an unloading holder should begin save preparation");
1764
1765 assert_eq!(preparation.finish(7), Some(7));
1766 assert!(
1767 holder.try_begin_save_preparation().is_some(),
1768 "finishing an uncontested phase must return the holder to the unloading lifecycle"
1769 );
1770 }
1771
1772 #[test]
1773 fn a_revived_holder_refuses_a_new_save_preparation() {
1774 let holder = test_holder();
1775 holder.begin_unloading();
1776
1777 holder.revive_from_unloading();
1778
1779 assert!(holder.try_begin_save_preparation().is_none());
1780 }
1781}