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
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    /// Gets the chunk position.
243    pub const fn get_pos(&self) -> ChunkPos {
244        self.pos
245    }
246
247    /// Gets the minimum Y coordinate of the world.
248    pub const fn min_y(&self) -> i32 {
249        self.min_y
250    }
251
252    /// Gets the total height of the world.
253    pub const fn height(&self) -> i32 {
254        self.height
255    }
256
257    /// Creates a new chunk holder.
258    #[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    /// Returns the current load ticket level.
319    pub fn load_level(&self) -> Option<ChunkTicketLevel> {
320        optional_ticket_level_from_raw(self.load_level.load(Ordering::Relaxed))
321    }
322
323    /// Stores the current load ticket level and returns the previous level.
324    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    /// Clears the current load ticket level.
329    pub(crate) fn clear_load_level(&self) {
330        self.load_level.store(NO_TICKET_LEVEL, Ordering::Relaxed);
331    }
332
333    /// Returns the current simulation ticket level.
334    pub fn simulation_level(&self) -> Option<ChunkTicketLevel> {
335        optional_ticket_level_from_raw(self.simulation_level.load(Ordering::Relaxed))
336    }
337
338    /// Stores the current simulation ticket level.
339    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    /// Updates the highest allowed generation status based on the ticket level.
363    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    /// Records a block change at the given position.
371    /// Returns `true` if this is the first change (chunk should be added to broadcast list).
372    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    /// Records a light-section change for a full chunk and marks saved light data dirty.
396    ///
397    /// Returns `true` if this is the first pending broadcast change for the chunk holder.
398    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    /// Marks saved light data dirty without queuing client-visible changes.
423    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    /// Returns whether there are pending changes to broadcast.
444    pub fn has_changes_to_broadcast(&self) -> bool {
445        self.queued_for_broadcast.load(Ordering::Acquire)
446    }
447
448    /// Allows later changes to enqueue this holder for a future broadcast.
449    pub fn clear_broadcast_queued(&self) {
450        self.queued_for_broadcast.store(false, Ordering::Release);
451    }
452
453    /// Takes all pending block changes, grouped by section index.
454    /// Returns a vec of (`section_index`, set of packed positions).
455    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    /// Takes all pending light-section changes.
471    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    /// Marks the holder's client-visible chunk packet content as changed.
480    pub fn mark_packet_content_changed(&self) {
481        self.packet_content_revision.fetch_add(1, Ordering::AcqRel);
482    }
483
484    /// Returns the current client-visible content revision.
485    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    /// Returns the number of sections in this chunk.
531    pub fn section_count(&self) -> usize {
532        self.changed_blocks_per_section.len()
533    }
534
535    /// Checks if the given status is disallowed.
536    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    /// Schedules a generation task for this chunk if needed.
545    ///
546    /// Returns `true` if a new task was actually scheduled, `false` if the chunk
547    /// already has a suitable task or is already at the target status.
548    #[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    /// Reschedules the chunk task to the given status.
583    #[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    /// Gets access to the chunk if it has reached the given status.
601    #[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    /// Gets the Full-only capability after Full status is published.
610    #[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    /// Waits until the chunk has reached the given status.
617    pub async fn await_chunk(&self, status: ChunkStatus) -> Option<&Chunk> {
618        loop {
619            // Register before checking the state so a concurrent publication
620            // cannot land between the check and the wait.
621            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    /// Waits until the chunk has reached the given status without reading chunk data.
636    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    /// Gets the published status of the chunk.
669    pub fn published_status(&self) -> Option<ChunkStatus> {
670        decoded_published_status(self.published_status.load(Ordering::Acquire))
671    }
672
673    /// Returns whether vanilla timed tickets may age for this chunk.
674    #[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    /// Moves an active holder into the unloading lifecycle.
687    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    /// Reserves the unloading holder while its immutable save input is assembled.
701    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    /// Attempts to reactivate an unloading holder without waiting for save preparation.
718    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    /// Applies a step to the chunk.
730    ///
731    /// Cancellation is handled structurally by the owning generation task: its
732    /// `run` loop races the whole `join_all` of dependency-wait futures against
733    /// its cancel token and drops them on cancellation, so the returned futures
734    /// don't each re-check it. A failed dependency surfaces as
735    /// `await_chunk_status` returning `None`.
736    ///
737    /// # Panics
738    /// Panics if the target status is not Empty and has no parent, or if the
739    /// chunk status is invalid during generation.
740    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            // Another task is already generating this chunk to `target_status`;
827            // just wait for it. Parent cancellation is handled by the owning
828            // task's run loop dropping this future; a failed dependency returns
829            // `None` from `await_claimed_chunk_status`.
830            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            // Keep the claim alive for the producer task so Drop can roll back abandoned work.
847            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    /// Upgrades the chunk to a full chunk.
1172    ///
1173    /// If the chunk is already Full (e.g., loaded from disk), this is a no-op.
1174    ///
1175    /// # Panics
1176    /// Panics if no chunk has been installed or Full runtime initialization repeats.
1177    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    /// Runs Full-load post-processing and returns the number of packed positions attempted.
1197    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    /// Finishes a generated status on the async scheduler after the Rayon task returns.
1225    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    /// Inserts a chunk into the holder with a specific status.
1252    /// This notifies watchers - use `insert_chunk_no_notify` + separate notification
1253    /// if calling from a rayon thread to avoid contention.
1254    ///
1255    /// # Panics
1256    ///
1257    /// Panics if `status` claims Full without initialized Full runtime state, or if
1258    /// initialized Full runtime state is paired with a lower status.
1259    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    /// Registers tick queues before Full status becomes observable to watchers.
1299    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            // Focused holder tests construct chunks without a live world. Real
1309            // loaded/generated chunks always carry the WorldGenContext world.
1310            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    /// Inserts a chunk into the holder without notifying watchers.
1332    /// The caller is responsible for notifying via the completion channel.
1333    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    /// Wakes all `await_chunk` watchers without changing the chunk result.
1341    /// This allows waiting futures to re-check `is_status_disallowed` and bail
1342    /// out during chunk unload.
1343    pub fn wake_all_watchers(&self) {
1344        self.status_changed.notify_waiters();
1345    }
1346
1347    /// Cancels the current generation task.
1348    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    /// Clears the current generation task if it is still the supplied task.
1358    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}