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