Skip to main content

steel_core/chunk/
chunk_holder.rs

1//! `ChunkHolder` manages chunk state and asynchronous generation tasks.
2use 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/// When `true`, each chunk generation stage sleeps 200 ms after completing.
17/// Set by the spawn progress display to make the terminal grid visible.
18#[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/// Exact ticking-readiness generation captured by concurrent consumers.
88#[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/// Pending light sections to send to players tracking a chunk.
129#[derive(Debug, Default, PartialEq, Eq)]
130pub struct ChangedLightSections {
131    /// Changed sky-light sections.
132    pub sky: Vec<SectionPos>,
133    /// Changed block-light sections.
134    pub block: Vec<SectionPos>,
135}
136
137impl ChangedLightSections {
138    /// Returns true when no light sections changed.
139    #[must_use]
140    pub const fn is_empty(&self) -> bool {
141        self.sky.is_empty() && self.block.is_empty()
142    }
143}
144
145/// Holds chunk data and coordinates asynchronous generation work.
146///
147/// `published_status` is released only after the corresponding data and Full
148/// tick containers are installed. Synchronous readers acquire it before
149/// reading `data`; `status_changed` wakes async waiters so they can re-check
150/// the atomic state.
151pub 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    /// The current loading ticket level of the chunk.
159    load_level: AtomicU8,
160    /// The current simulation ticket level of the chunk.
161    simulation_level: AtomicU8,
162    /// The highest status that has started work.
163    started_work: AtomicUsize,
164    /// Number of save dependencies that have not completed yet.
165    active_save_dependencies: AtomicUsize,
166    /// Coordinates unloading revival with the short immutable save-preparation phase.
167    save_lifecycle: AtomicU8,
168    /// The highest status that generation is allowed to reach.
169    highest_allowed_status: AtomicU8,
170    /// The minimum Y coordinate of the world.
171    min_y: i32,
172    /// The total height of the world.
173    height: i32,
174    /// Whether any sections have pending block changes.
175    has_changed_sections: AtomicBool,
176    /// Whether this holder is already queued for the next broadcast flush.
177    queued_for_broadcast: AtomicBool,
178    /// Monotonic revision for client-visible chunk packet content.
179    packet_content_revision: AtomicU64,
180    /// Packed ticking readiness generation. The low two bits store `TickingReadiness`.
181    ticking_readiness: AtomicU64,
182    /// Whether Full post-load initialization completed and was published for readiness.
183    full_status_initialized: AtomicBool,
184    /// Weak sink for Full status publication notifications.
185    full_publications: Weak<FullPublicationQueue>,
186    /// Per-section sets of changed block positions.
187    /// Index is `(block_y - min_y) / 16`.
188    changed_blocks_per_section: Box<[SyncMutex<FxHashSet<PackedSectionBlockPos>>]>,
189    /// Changed light sections grouped by light layer.
190    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
222/// Reservation of a holder's save-preparation phase.
223///
224/// [`Self::finish`] discards the snapshot if revival won. Its strong reference prevents a second
225/// preparation: `ChunkMap::process_unloads` requires a holder's strong count to be 1.
226pub(crate) struct ChunkSavePreparationGuard {
227    holder: Arc<ChunkHolder>,
228    /// Whether the lifecycle was already handed back, so `Drop` does not repeat it.
229    released: bool,
230}
231
232impl ChunkSavePreparationGuard {
233    /// Ends the phase, returning `value` only if no revival reclaimed the holder meanwhile.
234    #[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        // The preparation bailed before a snapshot existed, so nothing escaped.
248        let _ = self.holder.end_save_preparation();
249    }
250}
251
252impl ChunkHolder {
253    /// Gets the chunk position.
254    pub const fn get_pos(&self) -> ChunkPos {
255        self.pos
256    }
257
258    /// Gets the minimum Y coordinate of the world.
259    pub const fn min_y(&self) -> i32 {
260        self.min_y
261    }
262
263    /// Gets the total height of the world.
264    pub const fn height(&self) -> i32 {
265        self.height
266    }
267
268    /// Creates a new chunk holder.
269    #[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    /// Returns the current load ticket level.
330    pub fn load_level(&self) -> Option<ChunkTicketLevel> {
331        optional_ticket_level_from_raw(self.load_level.load(Ordering::Relaxed))
332    }
333
334    /// Stores the current load ticket level and returns the previous level.
335    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    /// Clears the current load ticket level.
340    pub(crate) fn clear_load_level(&self) {
341        self.load_level.store(NO_TICKET_LEVEL, Ordering::Relaxed);
342    }
343
344    /// Returns the current simulation ticket level.
345    pub fn simulation_level(&self) -> Option<ChunkTicketLevel> {
346        optional_ticket_level_from_raw(self.simulation_level.load(Ordering::Relaxed))
347    }
348
349    /// Stores the current simulation ticket level.
350    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    /// Updates the highest allowed generation status based on the ticket level.
374    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    /// Records a block change at the given position.
382    /// Returns `true` if this is the first change (chunk should be added to broadcast list).
383    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    /// Records a light-section change for a full chunk and marks saved light data dirty.
407    ///
408    /// Returns `true` if this is the first pending broadcast change for the chunk holder.
409    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    /// Marks saved light data dirty without queuing client-visible changes.
434    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    /// Returns whether there are pending changes to broadcast.
455    pub fn has_changes_to_broadcast(&self) -> bool {
456        self.queued_for_broadcast.load(Ordering::Acquire)
457    }
458
459    /// Allows later changes to enqueue this holder for a future broadcast.
460    pub fn clear_broadcast_queued(&self) {
461        self.queued_for_broadcast.store(false, Ordering::Release);
462    }
463
464    /// Takes all pending block changes, grouped by section index.
465    /// Returns a vec of (`section_index`, set of packed positions).
466    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    /// Takes all pending light-section changes.
482    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    /// Marks the holder's client-visible chunk packet content as changed.
491    pub fn mark_packet_content_changed(&self) {
492        self.packet_content_revision.fetch_add(1, Ordering::AcqRel);
493    }
494
495    /// Returns the current client-visible content revision.
496    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    /// Returns the number of sections in this chunk.
542    pub fn section_count(&self) -> usize {
543        self.changed_blocks_per_section.len()
544    }
545
546    /// Checks if the given status is disallowed.
547    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    /// Schedules a generation task for this chunk if needed.
556    ///
557    /// Returns `true` if a new task was actually scheduled, `false` if the chunk
558    /// already has a suitable task or is already at the target status.
559    #[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    /// Reschedules the chunk task to the given status.
594    #[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    /// Gets access to the chunk if it has reached the given status.
612    #[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    /// Gets the Full-only capability after Full status is published.
621    #[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    /// Waits until the chunk has reached the given status.
628    pub async fn await_chunk(&self, status: ChunkStatus) -> Option<&Chunk> {
629        loop {
630            // Register before checking the state so a concurrent publication
631            // cannot land between the check and the wait.
632            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    /// Waits until the chunk has reached the given status without reading chunk data.
647    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    /// Gets the published status of the chunk.
680    pub fn published_status(&self) -> Option<ChunkStatus> {
681        decoded_published_status(self.published_status.load(Ordering::Acquire))
682    }
683
684    /// Returns whether vanilla timed tickets may age for this chunk.
685    #[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    /// Moves an active holder into the unloading lifecycle.
698    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    /// Reserves the unloading holder while its immutable save input is assembled.
712    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    /// Leaves the preparing lifecycle, reporting whether the phase still owned the holder.
730    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    /// Reactivates an unloading holder even while a save snapshot is in flight.
742    /// A single `try_update` handles preparation finishing concurrently, which could make
743    /// separate CAS attempts for `PREPARING` and `UNLOADING` both fail.
744    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    /// Applies a step to the chunk.
757    ///
758    /// Cancellation is handled structurally by the owning generation task: its
759    /// `run` loop races the whole `join_all` of dependency-wait futures against
760    /// its cancel token and drops them on cancellation, so the returned futures
761    /// don't each re-check it. A failed dependency surfaces as
762    /// `await_chunk_status` returning `None`.
763    ///
764    /// # Panics
765    /// Panics if the target status is not Empty and has no parent, or if the
766    /// chunk status is invalid during generation.
767    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            // Another task is already generating this chunk to `target_status`;
854            // just wait for it. Parent cancellation is handled by the owning
855            // task's run loop dropping this future; a failed dependency returns
856            // `None` from `await_claimed_chunk_status`.
857            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            // Keep the claim alive for the producer task so Drop can roll back abandoned work.
874            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    /// Upgrades the chunk to a full chunk.
1199    ///
1200    /// If the chunk is already Full (e.g., loaded from disk), this is a no-op.
1201    ///
1202    /// # Panics
1203    /// Panics if no chunk has been installed or Full runtime initialization repeats.
1204    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    /// Runs Full-load post-processing and returns the number of packed positions attempted.
1224    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    /// Finishes a generated status on the async scheduler after the Rayon task returns.
1252    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    /// Inserts a chunk into the holder with a specific status.
1279    /// This notifies watchers - use `insert_chunk_no_notify` + separate notification
1280    /// if calling from a rayon thread to avoid contention.
1281    ///
1282    /// # Panics
1283    ///
1284    /// Panics if `status` claims Full without initialized Full runtime state, or if
1285    /// initialized Full runtime state is paired with a lower status.
1286    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    /// Registers tick queues before Full status becomes observable to watchers.
1326    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            // Focused holder tests construct chunks without a live world. Real
1336            // loaded/generated chunks always carry the WorldGenContext world.
1337            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    /// Inserts a chunk into the holder without notifying watchers.
1359    /// The caller is responsible for notifying via the completion channel.
1360    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    /// Wakes all `await_chunk` watchers without changing the chunk result.
1368    /// This allows waiting futures to re-check `is_status_disallowed` and bail
1369    /// out during chunk unload.
1370    pub fn wake_all_watchers(&self) {
1371        self.status_changed.notify_waiters();
1372    }
1373
1374    /// Cancels the current generation task.
1375    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    /// Clears the current generation task if it is still the supplied task.
1385    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}