Skip to main content

steel_core/chunk/
data.rs

1//! Chunk data retained from generation through Full runtime use.
2use std::fmt::{self, Formatter};
3use std::sync::{
4    Arc, OnceLock, Weak,
5    atomic::{AtomicBool, Ordering},
6};
7
8use parking_lot::{RwLockReadGuard, RwLockWriteGuard};
9use rustc_hash::FxHashMap;
10use steel_registry::{
11    REGISTRY,
12    blocks::{BlockRef, block_state_ext::BlockStateExt},
13    fluid::FluidRef,
14    vanilla_blocks,
15};
16use steel_utils::{
17    BlockPos, BlockStateId, ChunkPos, Downcast as _, DowncastType, ErasedType, SectionPos,
18    locks::{SyncMutex, SyncRwLock},
19    types::UpdateFlags,
20};
21
22use crate::behavior::{BLOCK_BEHAVIORS, BlockEntityCreation};
23use crate::block_entity::{BlockEntityLookup, BlockEntityStorage, SharedBlockEntity};
24use crate::chunk::{
25    full_chunk::FullChunkRuntime,
26    heightmap::{ChunkHeightmaps, HeightmapType},
27    light::{
28        ChunkLightData, ChunkSkyLightSources, LightSectionEmptinessChange,
29        has_different_light_properties,
30    },
31    section::Sections,
32    status::ChunkStatus,
33};
34use crate::entity::{EntityStorage, EntityStorageAddResult, SharedEntity};
35use crate::world::World;
36use crate::world::tick_scheduler::{
37    BlockTickList, ChunkTickContainer, ChunkTickLists, FluidTickList, TickPriority,
38};
39use steel_worldgen::structure::{StructureReferenceMap, StructureStartMap};
40
41pub(crate) fn empty_postprocessing(height: i32) -> Box<[Vec<u16>]> {
42    let section_count = (height / 16) as usize;
43    (0..section_count).map(|_| Vec::new()).collect()
44}
45
46pub(crate) fn postprocessing_from_disk(
47    height: i32,
48    mut postprocessing: Vec<Vec<u16>>,
49) -> Box<[Vec<u16>]> {
50    let section_count = (height / 16) as usize;
51    postprocessing.resize_with(section_count, Vec::new);
52    postprocessing.truncate(section_count);
53    postprocessing.into_boxed_slice()
54}
55
56#[derive(Default)]
57struct TransientGenerationState {
58    value: Option<Box<dyn ErasedType + Send + Sync>>,
59}
60
61impl fmt::Debug for TransientGenerationState {
62    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
63        formatter
64            .debug_struct("TransientGenerationState")
65            .field(
66                "type_key",
67                &self.value.as_deref().map(ErasedType::downcast_type_key),
68            )
69            .finish()
70    }
71}
72
73/// Canonical chunk storage retained across generation and Full runtime phases.
74#[derive(Debug)]
75pub struct Chunk {
76    /// The sections of the chunk.
77    pub sections: Sections,
78    /// The position of the chunk.
79    pub pos: ChunkPos,
80    /// Whether the chunk has been modified since last save.
81    /// Newly generated chunks start dirty.
82    pub dirty: AtomicBool,
83    /// Heightmaps retained across every generation phase.
84    pub(crate) heightmaps: SyncRwLock<ChunkHeightmaps>,
85    /// The minimum Y coordinate of the world this chunk belongs to.
86    min_y: i32,
87    /// The total height of the world.
88    height: i32,
89    /// Weak reference to the owning world.
90    level: Weak<World>,
91    /// Stable block-entity storage retained through Full promotion.
92    pub(crate) block_entities: BlockEntityStorage,
93    /// Entity staging storage closed and drained at Full promotion.
94    pub(crate) entities: EntityStorage,
95    /// Structure starts originating in this chunk.
96    pub structure_starts: SyncRwLock<StructureStartMap>,
97    /// References to structures from nearby origin chunks.
98    pub structure_references: SyncRwLock<StructureReferenceMap>,
99    /// Section-indexed packed offsets that need vanilla postprocessing after promotion.
100    pub postprocessing: SyncMutex<Box<[Vec<u16>]>>,
101    /// Stable block and fluid scheduled-tick storage retained through Full promotion.
102    pub(crate) scheduled_ticks: Arc<ChunkTickContainer>,
103    /// Vanilla skylight source edge cache for this chunk.
104    pub sky_light_sources: SyncRwLock<ChunkSkyLightSources>,
105    /// Chunk-owned light sections and section emptiness maps.
106    pub light: SyncRwLock<ChunkLightData>,
107    /// Full-only runtime state, installed once before Full publication.
108    full_runtime: OnceLock<Box<FullChunkRuntime>>,
109    /// Generator-owned state retained only between generation stages.
110    transient_generation_state: SyncMutex<TransientGenerationState>,
111}
112
113enum PendingPromotionCommit {
114    Retry,
115    Complete(Option<SharedBlockEntity>),
116}
117
118impl Chunk {
119    /// Creates a new proto chunk at the given position with empty sections.
120    #[must_use]
121    pub fn new(
122        sections: Sections,
123        pos: ChunkPos,
124        min_y: i32,
125        height: i32,
126        level: Weak<World>,
127    ) -> Self {
128        Self {
129            sections,
130            pos,
131            dirty: AtomicBool::new(true), // New chunks are always dirty
132            heightmaps: SyncRwLock::new(ChunkHeightmaps::empty()),
133            min_y,
134            height,
135            level,
136            block_entities: BlockEntityStorage::new(),
137            entities: EntityStorage::new(),
138            structure_starts: SyncRwLock::new(FxHashMap::default()),
139            structure_references: SyncRwLock::new(FxHashMap::default()),
140            postprocessing: SyncMutex::new(empty_postprocessing(height)),
141            scheduled_ticks: Arc::new(ChunkTickContainer::new_proto(ChunkTickLists::new(
142                BlockTickList::new_pending(),
143                FluidTickList::new_pending(),
144            ))),
145            sky_light_sources: SyncRwLock::new(ChunkSkyLightSources::for_valid_world_height(
146                min_y, height,
147            )),
148            light: SyncRwLock::new(ChunkLightData::for_valid_world_height(min_y, height)),
149            full_runtime: OnceLock::new(),
150            transient_generation_state: SyncMutex::new(TransientGenerationState::default()),
151        }
152    }
153
154    /// Creates a chunk that was loaded from disk.
155    ///
156    /// # Panics
157    ///
158    /// Panics when persisted light data does not match the loaded section range.
159    #[expect(
160        clippy::too_many_arguments,
161        reason = "disk rehydration mirrors the persisted proto chunk fields"
162    )]
163    #[must_use]
164    pub(crate) fn from_disk(
165        sections: Sections,
166        pos: ChunkPos,
167        status: ChunkStatus,
168        min_y: i32,
169        height: i32,
170        heightmaps: ChunkHeightmaps,
171        structure_starts: StructureStartMap,
172        structure_references: StructureReferenceMap,
173        postprocessing: Vec<Vec<u16>>,
174        block_ticks: BlockTickList,
175        fluid_ticks: FluidTickList,
176        level: Weak<World>,
177        mut light: ChunkLightData,
178    ) -> Self {
179        light
180            .refresh_emptiness_maps_from_sections(&sections)
181            .expect("invalid loaded proto chunk light emptiness map length");
182
183        let chunk = Self {
184            sections,
185            pos,
186            dirty: AtomicBool::new(false),
187            heightmaps: SyncRwLock::new(heightmaps),
188            min_y,
189            height,
190            level,
191            block_entities: BlockEntityStorage::new(),
192            entities: if status == ChunkStatus::Full {
193                EntityStorage::new_closed()
194            } else {
195                EntityStorage::new()
196            },
197            structure_starts: SyncRwLock::new(structure_starts),
198            structure_references: SyncRwLock::new(structure_references),
199            postprocessing: SyncMutex::new(postprocessing_from_disk(height, postprocessing)),
200            scheduled_ticks: Arc::new(if status == ChunkStatus::Full {
201                ChunkTickContainer::new(ChunkTickLists::new(block_ticks, fluid_ticks))
202            } else {
203                ChunkTickContainer::new_proto(ChunkTickLists::new(block_ticks, fluid_ticks))
204            }),
205            sky_light_sources: SyncRwLock::new(ChunkSkyLightSources::for_valid_world_height(
206                min_y, height,
207            )),
208            light: SyncRwLock::new(light),
209            full_runtime: OnceLock::new(),
210            transient_generation_state: SyncMutex::new(TransientGenerationState::default()),
211        };
212
213        if status >= ChunkStatus::InitializeLight {
214            chunk.initialize_light_sources();
215        }
216
217        chunk
218    }
219
220    /// Returns the minimum Y coordinate of the world.
221    #[must_use]
222    pub const fn min_y(&self) -> i32 {
223        self.min_y
224    }
225
226    /// Returns the total height of the world.
227    #[must_use]
228    pub const fn height(&self) -> i32 {
229        self.height
230    }
231
232    /// Returns the chunk position.
233    #[must_use]
234    pub const fn pos(&self) -> ChunkPos {
235        self.pos
236    }
237
238    /// Returns the stable section storage.
239    #[must_use]
240    pub const fn sections(&self) -> &Sections {
241        &self.sections
242    }
243
244    /// Returns whether this chunk has unsaved changes.
245    #[must_use]
246    pub fn is_dirty(&self) -> bool {
247        self.dirty.load(Ordering::Acquire)
248    }
249
250    /// Marks this chunk as needing persistence.
251    pub fn mark_dirty(&self) {
252        self.dirty.store(true, Ordering::Release);
253    }
254
255    /// Clears the dirty flag and returns its previous value.
256    pub fn take_dirty(&self) -> bool {
257        self.dirty.swap(false, Ordering::AcqRel)
258    }
259
260    /// Clears the dirty flag.
261    pub fn clear_dirty(&self) {
262        self.dirty.store(false, Ordering::Release);
263    }
264
265    /// Reads a block using coordinates relative to the chunk's minimum Y.
266    #[must_use]
267    pub fn get_relative_block(
268        &self,
269        relative_x: usize,
270        relative_y: usize,
271        relative_z: usize,
272    ) -> Option<BlockStateId> {
273        self.sections
274            .get_relative_block(relative_x, relative_y, relative_z)
275    }
276
277    /// Writes one generation block while preserving the current stage's side effects.
278    pub(crate) fn set_relative_block_for_generation(
279        &self,
280        status: ChunkStatus,
281        relative_x: usize,
282        relative_y: usize,
283        relative_z: usize,
284        value: BlockStateId,
285    ) {
286        if status >= ChunkStatus::InitializeLight {
287            self.sections
288                .set_relative_block(relative_x, relative_y, relative_z, value);
289            self.refresh_light_emptiness_maps();
290        } else {
291            self.sections
292                .set_relative_block_for_generation(relative_x, relative_y, relative_z, value);
293        }
294        self.mark_dirty();
295        self.update_status_heightmaps_after_block_change(
296            status,
297            relative_x,
298            self.min_y + relative_y as i32,
299            relative_z,
300            value,
301        );
302    }
303
304    /// Writes a batch of generation blocks.
305    pub(crate) fn write_block_batch_for_generation(
306        &self,
307        status: ChunkStatus,
308        blocks: &[(usize, usize, usize, BlockStateId)],
309    ) {
310        if blocks.is_empty() {
311            return;
312        }
313        if status < ChunkStatus::InitializeLight {
314            self.sections.write_block_batch(blocks);
315        } else {
316            self.sections.write_tracked_block_batch(blocks);
317            self.refresh_light_emptiness_maps();
318        }
319        self.mark_dirty();
320    }
321
322    /// Writes one column of generation blocks.
323    pub(crate) fn write_column_blocks_for_generation(
324        &self,
325        status: ChunkStatus,
326        x: usize,
327        z: usize,
328        blocks: &[(usize, BlockStateId)],
329    ) {
330        if blocks.is_empty() {
331            return;
332        }
333        if status < ChunkStatus::InitializeLight {
334            self.sections.write_column_blocks(x, z, blocks);
335        } else {
336            for &(relative_y, value) in blocks {
337                self.sections.set_relative_block(x, relative_y, z, value);
338            }
339            self.refresh_light_emptiness_maps();
340        }
341        self.mark_dirty();
342    }
343
344    /// Ensures the requested generation heightmaps exist.
345    pub(crate) fn prime_heightmaps(&self, heightmap_types: &[HeightmapType]) {
346        self.heightmaps.write().prime_from_sections(
347            heightmap_types,
348            self.min_y,
349            self.height,
350            &self.sections.sections,
351        );
352    }
353
354    /// Ensures every final heightmap exists before feature generation or promotion.
355    pub fn prime_final_heightmaps(&self) {
356        self.prime_heightmaps(HeightmapType::final_types());
357    }
358
359    /// Reads a generation heightmap, priming it lazily when needed.
360    #[must_use]
361    pub(crate) fn generation_height_at(
362        &self,
363        heightmap_type: HeightmapType,
364        local_x: usize,
365        local_z: usize,
366    ) -> i32 {
367        {
368            let heightmaps = self.heightmaps.read();
369            if let Some(heightmap) = heightmaps.get(heightmap_type) {
370                return heightmap.get_first_available(local_x, local_z);
371            }
372        }
373        self.prime_heightmaps(&[heightmap_type]);
374        let heightmaps = self.heightmaps.read();
375        let Some(heightmap) = heightmaps.get(heightmap_type) else {
376            panic!("heightmap {heightmap_type:?} missing after priming");
377        };
378        heightmap.get_first_available(local_x, local_z)
379    }
380
381    /// Returns the generation heightmaps retained by this chunk.
382    pub(crate) fn generation_heightmaps(&self) -> RwLockReadGuard<'_, ChunkHeightmaps> {
383        self.heightmaps.read()
384    }
385
386    /// Applies generation heightmap maintenance after direct writes in one column.
387    pub(crate) fn update_heightmaps_after_direct_column_writes(
388        &self,
389        status: ChunkStatus,
390        local_x: usize,
391        local_z: usize,
392        relative_writes: &[(usize, BlockStateId)],
393    ) {
394        if relative_writes.is_empty() {
395            return;
396        }
397        self.update_status_heightmaps_after_column_block_changes(
398            status,
399            local_x,
400            local_z,
401            relative_writes,
402        );
403    }
404
405    /// Returns a read guard for the skylight-source cache.
406    pub fn sky_light_sources(&self) -> RwLockReadGuard<'_, ChunkSkyLightSources> {
407        self.sky_light_sources.read()
408    }
409
410    /// Returns every block position that emits light in this chunk.
411    #[must_use]
412    pub fn block_light_sources(&self) -> Vec<BlockPos> {
413        self.sections.block_light_sources(self.pos, self.min_y)
414    }
415
416    /// Returns committed chunk light data.
417    pub fn light(&self) -> RwLockReadGuard<'_, ChunkLightData> {
418        self.light.read()
419    }
420
421    /// Returns mutable committed chunk light data.
422    pub(crate) fn light_mut(&self) -> RwLockWriteGuard<'_, ChunkLightData> {
423        self.light.write()
424    }
425
426    /// Returns structure starts originating in this chunk.
427    pub fn structure_starts(&self) -> RwLockReadGuard<'_, StructureStartMap> {
428        self.structure_starts.read()
429    }
430
431    /// Returns mutable structure starts originating in this chunk.
432    pub fn structure_starts_mut(&self) -> RwLockWriteGuard<'_, StructureStartMap> {
433        self.structure_starts.write()
434    }
435
436    /// Returns references to nearby structures.
437    pub fn structure_references(&self) -> RwLockReadGuard<'_, StructureReferenceMap> {
438        self.structure_references.read()
439    }
440
441    /// Returns mutable references to nearby structures.
442    pub fn structure_references_mut(&self) -> RwLockWriteGuard<'_, StructureReferenceMap> {
443        self.structure_references.write()
444    }
445
446    /// Installs Full-only runtime state before this chunk is published as Full.
447    pub(crate) fn initialize_full_runtime(
448        &self,
449        runtime: FullChunkRuntime,
450    ) -> Result<(), FullChunkRuntime> {
451        self.full_runtime
452            .set(Box::new(runtime))
453            .map_err(|runtime| *runtime)
454    }
455
456    /// Returns the Full-only runtime state after promotion or Full disk construction.
457    #[must_use]
458    pub(crate) fn full_runtime(&self) -> Option<&FullChunkRuntime> {
459        self.full_runtime.get().map(Box::as_ref)
460    }
461
462    /// Installs generator-owned state for reuse by later generation stages.
463    ///
464    /// # Panics
465    ///
466    /// Panics if the current generator has already installed transient state.
467    pub(crate) fn install_transient_generation_state<T>(&self, state: T)
468    where
469        T: DowncastType + Send + Sync,
470    {
471        let mut slot = self.transient_generation_state.lock();
472        if let Some(current) = slot.value.as_deref() {
473            panic!(
474                "chunk transient generation state {} was not consumed before installing {}",
475                current.downcast_type_key(),
476                T::TYPE_KEY
477            );
478        }
479        slot.value = Some(Box::new(state));
480    }
481
482    /// Borrows generator-owned state without extending its lifetime beyond the callback.
483    ///
484    /// Returns `None` when no state was installed, such as after reloading a partially
485    /// generated chunk from disk.
486    ///
487    /// # Panics
488    ///
489    /// Panics if another generator's state occupies this chunk.
490    pub(crate) fn with_transient_generation_state_mut<T, R>(
491        &self,
492        f: impl FnOnce(&mut T) -> R,
493    ) -> Option<R>
494    where
495        T: DowncastType + Send + Sync,
496    {
497        let mut slot = self.transient_generation_state.lock();
498        let state = slot.value.as_deref_mut()?;
499        let actual_key = state.downcast_type_key();
500        let Some(state) = state.downcast_mut::<T>() else {
501            panic!(
502                "chunk transient generation state type mismatch: expected {}, found {}",
503                T::TYPE_KEY,
504                actual_key
505            );
506        };
507        Some(f(state))
508    }
509
510    /// Removes generator-owned state and passes it to `f` before dropping it.
511    ///
512    /// The erased allocation is moved out before the callback, so it is also dropped if
513    /// the callback unwinds. `None` represents a cold continuation loaded from disk.
514    ///
515    /// # Panics
516    ///
517    /// Panics if another generator's state occupies this chunk.
518    pub(crate) fn consume_transient_generation_state<T, R>(
519        &self,
520        f: impl FnOnce(Option<&mut T>) -> R,
521    ) -> R
522    where
523        T: DowncastType + Send + Sync,
524    {
525        let state = self.transient_generation_state.lock().value.take();
526        let Some(mut state) = state else {
527            return f(None);
528        };
529        let actual_key = state.downcast_type_key();
530        let Some(state) = state.downcast_mut::<T>() else {
531            panic!(
532                "chunk transient generation state type mismatch: expected {}, found {}",
533                T::TYPE_KEY,
534                actual_key
535            );
536        };
537        f(Some(state))
538    }
539
540    /// Drops any generator-owned state that is no longer needed by the pipeline.
541    pub(crate) fn clear_transient_generation_state(&self) {
542        self.transient_generation_state.lock().value = None;
543    }
544
545    /// Vanilla `ProtoChunk.packOffsetCoordinates` for postprocessing offsets.
546    #[must_use]
547    pub const fn pack_postprocessing_offset(pos: BlockPos) -> u16 {
548        let x = (pos.0.x & 15) as u16;
549        let y = (pos.0.y & 15) as u16;
550        let z = (pos.0.z & 15) as u16;
551        x | (y << 4) | (z << 8)
552    }
553
554    /// Vanilla `ProtoChunk.unpackOffsetCoordinates` for postprocessing offsets.
555    #[must_use]
556    pub fn unpack_postprocessing_offset(
557        packed: u16,
558        section_y: i32,
559        chunk_pos: ChunkPos,
560    ) -> BlockPos {
561        let x = chunk_pos.0.x * 16 + i32::from(packed & 15);
562        let y = section_y * 16 + i32::from((packed >> 4) & 15);
563        let z = chunk_pos.0.y * 16 + i32::from((packed >> 8) & 15);
564        BlockPos::new(x, y, z)
565    }
566
567    /// Marks a block position for postprocessing after proto-to-full promotion.
568    pub(crate) fn mark_pos_for_postprocessing(&self, pos: BlockPos) {
569        let y = pos.0.y;
570        if y < self.min_y || y >= self.min_y + self.height {
571            return;
572        }
573
574        let section_index = self.get_section_index(y);
575        let packed = Self::pack_postprocessing_offset(pos);
576        self.postprocessing.lock()[section_index].push(packed);
577        self.mark_unsaved();
578    }
579
580    /// Gets the section index for a given Y coordinate.
581    #[must_use]
582    const fn get_section_index(&self, y: i32) -> usize {
583        ((y - self.min_y) / 16) as usize
584    }
585
586    /// Marks the chunk as unsaved.
587    fn mark_unsaved(&self) {
588        self.dirty.store(true, Ordering::Release);
589    }
590
591    /// Returns the weak reference to the world.
592    #[must_use]
593    pub(crate) fn level_weak(&self) -> Weak<World> {
594        Weak::clone(&self.level)
595    }
596
597    /// Returns a reference to the world if it is still alive.
598    #[must_use]
599    pub(crate) fn get_level(&self) -> Option<Arc<World>> {
600        self.level.upgrade()
601    }
602
603    /// Returns this chunk's block-entity storage.
604    #[must_use]
605    pub(crate) const fn block_entity_storage(&self) -> &BlockEntityStorage {
606        &self.block_entities
607    }
608
609    /// Returns this chunk's stable scheduled-tick container.
610    #[must_use]
611    pub(crate) const fn scheduled_tick_container(&self) -> &Arc<ChunkTickContainer> {
612        &self.scheduled_ticks
613    }
614
615    /// Fills the vanilla skylight-source cache from current section contents.
616    pub fn initialize_light_sources(&self) {
617        for section in &self.sections.sections {
618            section.write().recalculate_counts();
619        }
620        self.refresh_light_emptiness_maps();
621        self.sky_light_sources
622            .write()
623            .fill_from_sections(&self.sections);
624    }
625
626    /// Gets a block entity at the given position.
627    #[must_use]
628    pub(crate) fn get_block_entity(&self, pos: BlockPos) -> Option<SharedBlockEntity> {
629        self.block_entities.get(pos)
630    }
631
632    /// Stores a concrete proto block entity if its type accepts the live block state.
633    #[must_use]
634    pub(crate) fn set_block_entity(&self, block_entity: SharedBlockEntity) -> bool {
635        let pos = block_entity.get_block_pos();
636        if ChunkPos::from_block_pos(pos) != self.pos {
637            log::warn!(
638                "Trying to set block entity {} at {pos:?} in proto chunk {:?}",
639                block_entity.get_type().key,
640                self.pos,
641            );
642            return false;
643        }
644
645        loop {
646            let state = self.get_block_state(pos);
647            let valid = state.has_block_entity() && block_entity.is_valid_block_state(state);
648            if !valid {
649                let state_unchanged =
650                    self.with_locked_block_state(pos, |live_state| live_state == state);
651                if !state_unchanged {
652                    continue;
653                }
654                log::warn!(
655                    "Trying to set block entity {} at {pos:?}, but block {} does not accept that type",
656                    block_entity.get_type().key,
657                    state.get_block().key,
658                );
659                return false;
660            }
661
662            let committed = self.with_locked_block_state(pos, |live_state| {
663                if live_state != state {
664                    return false;
665                }
666                let _ = self.block_entities.set_without_lifecycle(&block_entity);
667                true
668            });
669            if !committed {
670                continue;
671            }
672            self.mark_unsaved();
673            return true;
674        }
675    }
676
677    /// Stores Vanilla's pending `DUMMY` marker for a worldgen-placed entity block.
678    pub(crate) fn set_pending_block_entity(&self, pos: BlockPos) {
679        if ChunkPos::from_block_pos(pos) != self.pos {
680            log::warn!(
681                "Trying to set a pending block entity at {pos:?} in proto chunk {:?}",
682                self.pos,
683            );
684            return;
685        }
686        if self.block_entities.set_pending(pos) {
687            self.mark_unsaved();
688        }
689    }
690
691    /// Stores a pending marker only while `expected_state` is still live.
692    pub(crate) fn set_pending_block_entity_if_state(
693        &self,
694        pos: BlockPos,
695        expected_state: BlockStateId,
696    ) -> bool {
697        if ChunkPos::from_block_pos(pos) != self.pos {
698            return false;
699        }
700        let inserted = self.with_locked_block_state(pos, |live_state| {
701            live_state == expected_state && self.block_entities.set_pending(pos)
702        });
703        if inserted {
704            self.mark_unsaved();
705        }
706        inserted
707    }
708
709    /// Returns pending `DUMMY` positions for promotion or serialization.
710    #[must_use]
711    pub fn pending_block_entity_positions(&self) -> Vec<BlockPos> {
712        self.block_entities.pending_positions()
713    }
714
715    /// Promotes a pending worldgen `DUMMY` on explicit region access without ticking it.
716    pub(crate) fn promote_pending_block_entity(&self, pos: BlockPos) -> Option<SharedBlockEntity> {
717        if ChunkPos::from_block_pos(pos) != self.pos {
718            return None;
719        }
720        loop {
721            match self.block_entities.lookup(pos) {
722                BlockEntityLookup::Concrete(block_entity) => return Some(block_entity),
723                BlockEntityLookup::Pending => {}
724                BlockEntityLookup::Absent => return None,
725            }
726
727            let state = self.get_block_state(pos);
728            if !state.has_block_entity() {
729                let state_unchanged =
730                    self.with_locked_block_state(pos, |live_state| live_state == state);
731                if state_unchanged {
732                    return None;
733                }
734                continue;
735            }
736
737            let behavior = BLOCK_BEHAVIORS.get_behavior(state.get_block());
738            let creation = behavior.new_block_entity(Weak::clone(&self.level), pos, state);
739            match self.commit_pending_creation(pos, state, creation) {
740                PendingPromotionCommit::Retry => {}
741                PendingPromotionCommit::Complete(block_entity) => return block_entity,
742            }
743        }
744    }
745
746    fn commit_pending_creation(
747        &self,
748        pos: BlockPos,
749        expected_state: BlockStateId,
750        creation: BlockEntityCreation,
751    ) -> PendingPromotionCommit {
752        let BlockEntityCreation::Created(block_entity) = creation else {
753            // Proto chunks retain markers for both intentional null factories and Steel's
754            // deferred implementations. Full promotion resolves their final semantics.
755            return self.with_locked_block_state(pos, |live_state| {
756                if live_state == expected_state {
757                    PendingPromotionCommit::Complete(None)
758                } else {
759                    PendingPromotionCommit::Retry
760                }
761            });
762        };
763        let valid = block_entity.get_block_pos() == pos
764            && ChunkPos::from_block_pos(pos) == self.pos
765            && block_entity.is_valid_block_state(expected_state);
766        self.with_locked_block_state(pos, |live_state| {
767            if live_state != expected_state {
768                return PendingPromotionCommit::Retry;
769            }
770            if !valid {
771                return PendingPromotionCommit::Complete(None);
772            }
773            PendingPromotionCommit::Complete(
774                self.block_entities
775                    .promote_without_lifecycle(pos, block_entity),
776            )
777        })
778    }
779
780    /// Removes a block entity at the given position.
781    pub(crate) fn remove_block_entity(&self, pos: BlockPos) {
782        self.block_entities.remove_without_lifecycle(pos);
783        self.mark_unsaved();
784    }
785
786    /// Removes an entity or marker only while `expected_state` is still live.
787    pub(crate) fn remove_block_entity_if_state(
788        &self,
789        pos: BlockPos,
790        expected_state: BlockStateId,
791    ) -> bool {
792        if ChunkPos::from_block_pos(pos) != self.pos {
793            return false;
794        }
795        let removed = self.with_locked_block_state(pos, |live_state| {
796            live_state == expected_state && self.block_entities.remove_without_lifecycle(pos)
797        });
798        if removed {
799            self.mark_unsaved();
800        }
801        removed
802    }
803
804    /// Drops every block entity without Full-chunk lifecycle callbacks or dirtying.
805    pub(crate) fn clear_all_block_entities(&self) {
806        self.block_entities.clear_without_lifecycle();
807    }
808
809    /// Returns all block entities in this proto chunk.
810    #[must_use]
811    pub fn get_block_entities(&self) -> Vec<SharedBlockEntity> {
812        self.block_entities.get_all_without_lifecycle_filter()
813    }
814
815    /// Adds an entity to proto storage.
816    pub(crate) fn add_entity(&self, entity: SharedEntity) -> bool {
817        match self.entities.add(entity) {
818            EntityStorageAddResult::Staged => {
819                self.mark_unsaved();
820                true
821            }
822            EntityStorageAddResult::Closed(entity) => {
823                // A retained worldgen reference becomes Vanilla's false-write
824                // ImposterProtoChunk after promotion. Its addEntity call drops
825                // the entity, while WorldGenRegion.addFreshEntity still reports
826                // success.
827                drop(entity);
828                true
829            }
830        }
831    }
832
833    /// Returns all entities in this proto chunk.
834    #[must_use]
835    pub fn get_entities(&self) -> Vec<SharedEntity> {
836        self.entities.get_all()
837    }
838
839    /// Returns entities that should be persisted from this proto chunk.
840    #[must_use]
841    pub(crate) fn get_saveable_entities(&self) -> Vec<SharedEntity> {
842        self.entities.get_saveable_entities()
843    }
844
845    /// Schedules a block tick in proto storage.
846    ///
847    /// Vanilla `ProtoChunkTicks.schedule(ScheduledTick)` stores a saved tick with delay `0`,
848    /// so worldgen-scheduled proto ticks run after promotion instead of preserving the
849    /// requested delay from generation time.
850    pub(crate) fn schedule_block_tick(
851        &self,
852        pos: BlockPos,
853        block: BlockRef,
854        priority: TickPriority,
855    ) {
856        if self
857            .scheduled_ticks
858            .schedule_pending_block(block, pos, priority)
859            == Some(true)
860        {
861            self.mark_unsaved();
862        }
863    }
864
865    /// Schedules a fluid tick in proto storage.
866    ///
867    /// See [`Self::schedule_block_tick`] for why proto ticks use delay `0`.
868    pub(crate) fn schedule_fluid_tick(
869        &self,
870        pos: BlockPos,
871        fluid: FluidRef,
872        priority: TickPriority,
873    ) {
874        if self
875            .scheduled_ticks
876            .schedule_pending_fluid(fluid, pos, priority)
877            == Some(true)
878        {
879            self.mark_unsaved();
880        }
881    }
882
883    /// Sets a block state at the given position.
884    ///
885    /// Returns the old block state at the position, or `VOID_AIR` if out of bounds.
886    pub(crate) fn set_block_state_for_generation(
887        &self,
888        status: ChunkStatus,
889        pos: BlockPos,
890        state: BlockStateId,
891        _flags: UpdateFlags,
892    ) -> Option<BlockStateId> {
893        let y = pos.0.y;
894
895        if y < self.min_y || y >= self.min_y + self.height {
896            return Some(
897                REGISTRY
898                    .blocks
899                    .get_default_state_id(&vanilla_blocks::VOID_AIR),
900            );
901        }
902
903        let local_x = (pos.0.x & 15) as usize;
904        let local_y = (y & 15) as usize;
905        let local_z = (pos.0.z & 15) as usize;
906
907        let section_index = self.get_section_index(y);
908        let section = &self.sections.sections[section_index];
909        let (old_state, empty_section_changed_to) = {
910            let mut section_guard = section.write();
911            if status >= ChunkStatus::InitializeLight {
912                let was_empty = section_guard.is_empty();
913                let old_state = section_guard.set_block_state(local_x, local_y, local_z, state);
914                let is_empty = section_guard.is_empty();
915                let empty_section_changed_to = (was_empty != is_empty).then_some(is_empty);
916                (old_state, empty_section_changed_to)
917            } else {
918                (
919                    section_guard.set_block_state_for_generation(local_x, local_y, local_z, state),
920                    None,
921                )
922            }
923        };
924
925        if old_state == state {
926            return None;
927        }
928
929        if status >= ChunkStatus::InitializeLight {
930            let empty_section_change = empty_section_changed_to.map(|is_empty| {
931                self.update_light_section_emptiness(y, is_empty);
932                LightSectionEmptinessChange {
933                    section_pos: SectionPos::new(
934                        self.pos.0.x,
935                        SectionPos::block_to_section_coord(y),
936                        self.pos.0.y,
937                    ),
938                    empty: is_empty,
939                }
940            });
941
942            let light_properties_changed = has_different_light_properties(old_state, state);
943            if light_properties_changed {
944                self.update_sky_light_sources(local_x, y, local_z);
945            }
946            if status >= ChunkStatus::Light
947                && (light_properties_changed || empty_section_change.is_some())
948                && let Some(level) = self.level.upgrade()
949            {
950                level.queue_light_change_after_block_set(
951                    pos,
952                    old_state,
953                    state,
954                    empty_section_change,
955                );
956            }
957        }
958
959        self.update_status_heightmaps_after_block_change(status, local_x, y, local_z, state);
960
961        self.mark_unsaved();
962        Some(old_state)
963    }
964
965    fn update_light_section_emptiness(&self, y: i32, is_empty: bool) {
966        let section_y = SectionPos::block_to_section_coord(y);
967        self.light.write().set_section_empty(section_y, is_empty);
968    }
969
970    fn update_sky_light_sources(&self, local_x: usize, y: i32, local_z: usize) {
971        let chunk_min_x = self.pos.0.x * 16;
972        let chunk_min_z = self.pos.0.y * 16;
973        self.sky_light_sources
974            .write()
975            .update(local_x, y, local_z, |scan_x, scan_y, scan_z| {
976                self.get_block_state(BlockPos::new(
977                    chunk_min_x + scan_x as i32,
978                    scan_y,
979                    chunk_min_z + scan_z as i32,
980                ))
981            });
982    }
983
984    pub(crate) fn refresh_light_emptiness_maps(&self) {
985        self.light
986            .write()
987            .refresh_emptiness_maps_from_sections(&self.sections)
988            .expect("invalid proto chunk light emptiness map length");
989    }
990
991    /// Applies the heightmap side effect for an optimized direct section write.
992    ///
993    /// Use this only for generation paths that intentionally bypass
994    /// [`Self::set_block_state`] but still need vanilla heightmap maintenance.
995    pub(crate) fn update_status_heightmaps_after_block_change(
996        &self,
997        status: ChunkStatus,
998        local_x: usize,
999        y: i32,
1000        local_z: usize,
1001        state: BlockStateId,
1002    ) {
1003        self.update_heightmaps_after_block_change(
1004            status.heightmaps_after(),
1005            local_x,
1006            y,
1007            local_z,
1008            state,
1009        );
1010    }
1011
1012    pub(crate) fn update_status_heightmaps_after_column_block_changes(
1013        &self,
1014        status: ChunkStatus,
1015        local_x: usize,
1016        local_z: usize,
1017        relative_writes: &[(usize, BlockStateId)],
1018    ) {
1019        self.update_heightmaps_after_column_block_changes(
1020            status.heightmaps_after(),
1021            local_x,
1022            local_z,
1023            relative_writes,
1024        );
1025    }
1026
1027    fn update_heightmaps_after_block_change(
1028        &self,
1029        heightmap_types: &[HeightmapType],
1030        local_x: usize,
1031        y: i32,
1032        local_z: usize,
1033        state: BlockStateId,
1034    ) {
1035        let min_y = self.min_y;
1036        let height = self.height;
1037        let sections = &self.sections;
1038
1039        let get_block = |lx: usize, scan_y: i32, lz: usize| {
1040            let scan_section_index = ((scan_y - min_y) / 16) as usize;
1041            let scan_local_y = ((scan_y - min_y) % 16) as usize;
1042            sections.sections[scan_section_index]
1043                .read()
1044                .states()
1045                .get(lx, scan_local_y, lz)
1046        };
1047
1048        let mut heightmaps = self.heightmaps.write();
1049        heightmaps.prime_from_sections(heightmap_types, min_y, height, &sections.sections);
1050
1051        for &hm_type in heightmap_types {
1052            let Some(heightmap) = heightmaps.get_mut(hm_type) else {
1053                panic!("heightmap {hm_type:?} missing after priming");
1054            };
1055            heightmap.update(local_x, y, local_z, state, get_block);
1056        }
1057    }
1058
1059    fn update_heightmaps_after_column_block_changes(
1060        &self,
1061        heightmap_types: &[HeightmapType],
1062        local_x: usize,
1063        local_z: usize,
1064        relative_writes: &[(usize, BlockStateId)],
1065    ) {
1066        if relative_writes.is_empty() {
1067            return;
1068        }
1069
1070        let min_y = self.min_y;
1071        let height = self.height;
1072        let sections = &self.sections;
1073
1074        let get_block = |lx: usize, scan_y: i32, lz: usize| {
1075            let scan_section_index = ((scan_y - min_y) / 16) as usize;
1076            let scan_local_y = ((scan_y - min_y) % 16) as usize;
1077            sections.sections[scan_section_index]
1078                .read()
1079                .states()
1080                .get(lx, scan_local_y, lz)
1081        };
1082
1083        let mut heightmaps = self.heightmaps.write();
1084        heightmaps.prime_from_sections(heightmap_types, min_y, height, &sections.sections);
1085
1086        for &(relative_y, state) in relative_writes {
1087            let y = min_y + relative_y as i32;
1088            for &hm_type in heightmap_types {
1089                let Some(heightmap) = heightmaps.get_mut(hm_type) else {
1090                    panic!("heightmap {hm_type:?} missing after priming");
1091                };
1092                heightmap.update(local_x, y, local_z, state, get_block);
1093            }
1094        }
1095    }
1096
1097    /// Gets a block state at the given position.
1098    #[must_use]
1099    pub fn get_block_state(&self, pos: BlockPos) -> BlockStateId {
1100        let y = pos.0.y;
1101
1102        // Check bounds
1103        if y < self.min_y || y >= self.min_y + self.height {
1104            // Out of bounds - return default air
1105            return REGISTRY.blocks.get_default_state_id(&vanilla_blocks::AIR);
1106        }
1107
1108        let section_index = self.get_section_index(y);
1109        let section = &self.sections.sections[section_index];
1110        let section_guard = section.read();
1111
1112        let local_x = (pos.0.x & 15) as usize;
1113        let local_y = (y & 15) as usize;
1114        let local_z = (pos.0.z & 15) as usize;
1115
1116        section_guard.states().get(local_x, local_y, local_z)
1117    }
1118
1119    fn with_locked_block_state<R>(&self, pos: BlockPos, f: impl FnOnce(BlockStateId) -> R) -> R {
1120        let y = pos.y();
1121        if y < self.min_y || y >= self.min_y + self.height {
1122            return f(REGISTRY.blocks.get_default_state_id(&vanilla_blocks::AIR));
1123        }
1124
1125        let section = self.sections.sections[self.get_section_index(y)].read();
1126        let state = section.states().get(
1127            (pos.x() & 15) as usize,
1128            (y & 15) as usize,
1129            (pos.z() & 15) as usize,
1130        );
1131        f(state)
1132    }
1133}
1134
1135#[cfg(test)]
1136mod tests {
1137    use std::sync::{
1138        Arc, Weak,
1139        atomic::{AtomicUsize, Ordering},
1140    };
1141
1142    use super::{Chunk, PendingPromotionCommit};
1143    use crate::behavior::{BlockEntityCreation, init_behaviors};
1144    use crate::block_entity::{
1145        BlockEntityLifecycleExt as _, SharedBlockEntity,
1146        entities::{SignBlockEntity, UnimplementedBlockEntity},
1147        init_block_entities,
1148    };
1149    use crate::chunk::{
1150        section::{ChunkSection, Sections},
1151        status::ChunkStatus,
1152    };
1153    use crate::world::tick_scheduler::TickPriority;
1154    use steel_registry::{init_vanilla_registry, vanilla_block_entity_types, vanilla_blocks};
1155    use steel_utils::{BlockPos, ChunkPos, DowncastType, DowncastTypeKey, types::UpdateFlags};
1156
1157    struct DropSentinel(Arc<AtomicUsize>);
1158
1159    // SAFETY: This key uniquely identifies the test-only sentinel type.
1160    unsafe impl DowncastType for DropSentinel {
1161        const TYPE_KEY: DowncastTypeKey =
1162            DowncastTypeKey::new("steel:test/chunk/transient_generation_state");
1163    }
1164
1165    impl Drop for DropSentinel {
1166        fn drop(&mut self) {
1167            self.0.fetch_add(1, Ordering::Relaxed);
1168        }
1169    }
1170
1171    #[test]
1172    fn transient_generation_state_survives_borrow_and_is_consumed_once() {
1173        init_vanilla_registry();
1174        let chunk = Chunk::new(
1175            Sections::from_owned(vec![ChunkSection::new_empty()].into_boxed_slice()),
1176            ChunkPos::new(0, 0),
1177            0,
1178            16,
1179            Weak::new(),
1180        );
1181        let drops = Arc::new(AtomicUsize::new(0));
1182        chunk.install_transient_generation_state(DropSentinel(Arc::clone(&drops)));
1183
1184        assert!(
1185            chunk
1186                .with_transient_generation_state_mut::<DropSentinel, _>(|_| ())
1187                .is_some()
1188        );
1189        assert_eq!(drops.load(Ordering::Relaxed), 0);
1190
1191        let present =
1192            chunk.consume_transient_generation_state::<DropSentinel, _>(|state| state.is_some());
1193        assert!(present);
1194        assert_eq!(drops.load(Ordering::Relaxed), 1);
1195        assert!(
1196            !chunk
1197                .consume_transient_generation_state::<DropSentinel, _>(|state| { state.is_some() })
1198        );
1199        assert_eq!(drops.load(Ordering::Relaxed), 1);
1200    }
1201
1202    #[test]
1203    fn full_promotion_drops_leftover_transient_generation_state() {
1204        init_vanilla_registry();
1205        init_behaviors();
1206        let chunk = Chunk::new(
1207            Sections::from_owned(vec![ChunkSection::new_empty()].into_boxed_slice()),
1208            ChunkPos::new(0, 0),
1209            0,
1210            16,
1211            Weak::new(),
1212        );
1213        let drops = Arc::new(AtomicUsize::new(0));
1214        chunk.install_transient_generation_state(DropSentinel(Arc::clone(&drops)));
1215
1216        let _ = chunk.promote_to_full();
1217
1218        assert_eq!(drops.load(Ordering::Relaxed), 1);
1219    }
1220
1221    #[test]
1222    fn postprocessing_offset_pack_unpack_matches_vanilla_layout() {
1223        let chunk_pos = ChunkPos::new(-2, 1);
1224        let section_y = -4;
1225        let pos = BlockPos::new(-17, -63, 31);
1226
1227        let packed = Chunk::pack_postprocessing_offset(pos);
1228
1229        assert_eq!(packed, 15 | (1 << 4) | (15 << 8));
1230        assert_eq!(
1231            Chunk::unpack_postprocessing_offset(packed, section_y, chunk_pos),
1232            pos
1233        );
1234    }
1235
1236    #[test]
1237    fn proto_scheduled_block_ticks_use_vanilla_zero_delay() {
1238        init_vanilla_registry();
1239        let proto = Chunk::new(
1240            Sections::from_owned(vec![ChunkSection::new_empty()].into_boxed_slice()),
1241            ChunkPos::new(0, 0),
1242            0,
1243            16,
1244            Weak::new(),
1245        );
1246        let pos = BlockPos::new(3, 4, 5);
1247
1248        proto.schedule_block_tick(pos, &vanilla_blocks::DIRT, TickPriority::Normal);
1249
1250        let Some(snapshot) = proto.scheduled_ticks.snapshot(0) else {
1251            panic!("proto chunk scheduled ticks should remain available");
1252        };
1253        let Some(tick) = snapshot.block.first() else {
1254            panic!("proto chunk should store scheduled block tick");
1255        };
1256
1257        assert_eq!(tick.pos, pos);
1258        assert_eq!(tick.tick_type, &vanilla_blocks::DIRT);
1259        assert_eq!(tick.delay, 0);
1260        assert_eq!(tick.priority, TickPriority::Normal);
1261    }
1262
1263    #[test]
1264    fn full_promotion_retains_and_closes_proto_scheduled_tick_container() {
1265        init_vanilla_registry();
1266        let proto = Chunk::new(
1267            Sections::from_owned(vec![ChunkSection::new_empty()].into_boxed_slice()),
1268            ChunkPos::new(0, 0),
1269            0,
1270            16,
1271            Weak::new(),
1272        );
1273        let scheduled_ticks = Arc::clone(&proto.scheduled_ticks);
1274        let pos = BlockPos::new(3, 4, 5);
1275        proto.schedule_block_tick(pos, &vanilla_blocks::DIRT, TickPriority::Normal);
1276
1277        let full = proto.promote_to_full().chunk;
1278
1279        assert_eq!(
1280            scheduled_ticks.schedule_pending_block(
1281                &vanilla_blocks::DIRT,
1282                BlockPos::new(6, 7, 8),
1283                TickPriority::Normal,
1284            ),
1285            None
1286        );
1287        assert!(Arc::ptr_eq(
1288            &scheduled_ticks,
1289            full.scheduled_tick_container()
1290        ));
1291        let snapshot = full.scheduled_tick_snapshot();
1292        assert_eq!(snapshot.block.len(), 1);
1293        assert_eq!(snapshot.block[0].pos, pos);
1294    }
1295
1296    #[test]
1297    fn proto_chunk_preserves_distinct_air_states_in_empty_sections() {
1298        init_vanilla_registry();
1299        init_behaviors();
1300        let proto = Chunk::new(
1301            Sections::from_owned(vec![ChunkSection::new_empty()].into_boxed_slice()),
1302            ChunkPos::new(0, 0),
1303            0,
1304            16,
1305            Weak::new(),
1306        );
1307        let pos = BlockPos::new(3, 4, 5);
1308        let cave_air = vanilla_blocks::CAVE_AIR.default_state();
1309
1310        proto.set_block_state_for_generation(
1311            ChunkStatus::Empty,
1312            pos,
1313            cave_air,
1314            UpdateFlags::UPDATE_CLIENTS,
1315        );
1316
1317        assert_eq!(proto.get_block_state(pos), cave_air);
1318    }
1319
1320    #[test]
1321    fn pre_light_block_writes_defer_counts_until_light_initialization() {
1322        init_vanilla_registry();
1323        init_behaviors();
1324        let proto = Chunk::new(
1325            Sections::from_owned(vec![ChunkSection::new_empty()].into_boxed_slice()),
1326            ChunkPos::new(0, 0),
1327            0,
1328            16,
1329            Weak::new(),
1330        );
1331        let pos = BlockPos::new(3, 4, 5);
1332        let stone = vanilla_blocks::STONE.default_state();
1333        let air = vanilla_blocks::AIR.default_state();
1334
1335        proto
1336            .sections
1337            .set_relative_block_for_generation(3, 4, 5, stone);
1338        assert_eq!(proto.sections.sections[0].read().non_empty_block_count(), 0);
1339
1340        assert_eq!(
1341            proto.set_block_state_for_generation(
1342                ChunkStatus::Empty,
1343                pos,
1344                air,
1345                UpdateFlags::UPDATE_CLIENTS,
1346            ),
1347            Some(stone)
1348        );
1349        assert_eq!(proto.get_block_state(pos), air);
1350
1351        assert_eq!(
1352            proto.set_block_state_for_generation(
1353                ChunkStatus::Empty,
1354                pos,
1355                stone,
1356                UpdateFlags::UPDATE_CLIENTS,
1357            ),
1358            Some(air)
1359        );
1360        assert_eq!(proto.sections.sections[0].read().non_empty_block_count(), 0);
1361
1362        proto.initialize_light_sources();
1363        assert_eq!(proto.sections.sections[0].read().non_empty_block_count(), 1);
1364    }
1365
1366    #[test]
1367    fn proto_mutation_defers_lifecycle_and_promotion_revalidates_concrete_entities() {
1368        init_vanilla_registry();
1369        init_behaviors();
1370        let proto = Chunk::new(
1371            Sections::from_owned(vec![ChunkSection::new_empty()].into_boxed_slice()),
1372            ChunkPos::new(0, 0),
1373            0,
1374            16,
1375            Weak::new(),
1376        );
1377        let pos = BlockPos::new(3, 4, 5);
1378        let sign = vanilla_blocks::OAK_SIGN.default_state();
1379        assert!(
1380            proto
1381                .set_block_state_for_generation(
1382                    ChunkStatus::Empty,
1383                    pos,
1384                    sign,
1385                    UpdateFlags::UPDATE_NONE,
1386                )
1387                .is_some()
1388        );
1389        let entity: SharedBlockEntity = Arc::new(SignBlockEntity::new(Weak::new(), pos, sign));
1390        assert!(proto.set_block_entity(Arc::clone(&entity)));
1391
1392        assert_eq!(
1393            proto.set_block_state_for_generation(
1394                ChunkStatus::Empty,
1395                pos,
1396                vanilla_blocks::STONE.default_state(),
1397                UpdateFlags::UPDATE_NONE,
1398            ),
1399            Some(sign)
1400        );
1401        assert!(proto.get_block_entity(pos).is_some());
1402
1403        let full = proto.promote_to_full().chunk;
1404        assert!(!entity.is_removed());
1405        assert!(full.get_block_entity(pos).is_none());
1406    }
1407
1408    #[test]
1409    fn proto_storage_preserves_removed_entries_until_full_promotion() {
1410        init_vanilla_registry();
1411        init_behaviors();
1412        let proto = 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        let pos = BlockPos::new(3, 4, 5);
1420        let sign = vanilla_blocks::OAK_SIGN.default_state();
1421        assert!(
1422            proto
1423                .set_block_state_for_generation(
1424                    ChunkStatus::Empty,
1425                    pos,
1426                    sign,
1427                    UpdateFlags::UPDATE_NONE,
1428                )
1429                .is_some()
1430        );
1431        let entity: SharedBlockEntity = Arc::new(SignBlockEntity::new(Weak::new(), pos, sign));
1432        entity.set_removed();
1433        assert!(proto.set_block_entity(Arc::clone(&entity)));
1434
1435        assert_eq!(proto.get_block_entities().len(), 1);
1436        assert_eq!(
1437            proto
1438                .block_entities
1439                .save_snapshot_without_lifecycle_filter()
1440                .0
1441                .len(),
1442            1
1443        );
1444
1445        let promotion = proto.promote_to_full();
1446        let full = promotion.chunk;
1447        assert!(!entity.is_removed());
1448        assert!(full.get_block_entity(pos).is_some());
1449    }
1450
1451    #[test]
1452    fn pending_proto_entity_promotes_without_running_live_lifecycle() {
1453        init_vanilla_registry();
1454        init_behaviors();
1455        init_block_entities();
1456        let proto = Chunk::new(
1457            Sections::from_owned(vec![ChunkSection::new_empty()].into_boxed_slice()),
1458            ChunkPos::new(0, 0),
1459            0,
1460            16,
1461            Weak::new(),
1462        );
1463        let pos = BlockPos::new(3, 4, 5);
1464        let sign = vanilla_blocks::OAK_SIGN.default_state();
1465        assert!(
1466            proto
1467                .set_block_state_for_generation(
1468                    ChunkStatus::Empty,
1469                    pos,
1470                    sign,
1471                    UpdateFlags::UPDATE_NONE,
1472                )
1473                .is_some()
1474        );
1475        proto.set_pending_block_entity(pos);
1476
1477        assert!(proto.promote_pending_block_entity(pos).is_some());
1478        assert_eq!(proto.pending_block_entity_positions().len(), 0);
1479    }
1480
1481    #[test]
1482    fn conditional_proto_marker_mutation_rejects_stale_worldgen_state() {
1483        init_vanilla_registry();
1484        init_behaviors();
1485        let proto = Chunk::new(
1486            Sections::from_owned(vec![ChunkSection::new_empty()].into_boxed_slice()),
1487            ChunkPos::new(0, 0),
1488            0,
1489            16,
1490            Weak::new(),
1491        );
1492        let pos = BlockPos::new(3, 4, 5);
1493        let copper = vanilla_blocks::COPPER_CHEST.default_state();
1494        let exposed = vanilla_blocks::EXPOSED_COPPER_CHEST.default_state();
1495        assert!(
1496            proto
1497                .set_block_state_for_generation(
1498                    ChunkStatus::Empty,
1499                    pos,
1500                    exposed,
1501                    UpdateFlags::UPDATE_NONE,
1502                )
1503                .is_some()
1504        );
1505
1506        assert!(!proto.set_pending_block_entity_if_state(pos, copper));
1507        assert_eq!(proto.pending_block_entity_positions().len(), 0);
1508        assert!(proto.set_pending_block_entity_if_state(pos, exposed));
1509
1510        let stone = vanilla_blocks::STONE.default_state();
1511        assert_eq!(
1512            proto.set_block_state_for_generation(
1513                ChunkStatus::Empty,
1514                pos,
1515                stone,
1516                UpdateFlags::UPDATE_NONE,
1517            ),
1518            Some(exposed)
1519        );
1520        assert!(!proto.remove_block_entity_if_state(pos, exposed));
1521        assert_eq!(proto.pending_block_entity_positions(), [pos]);
1522        assert!(proto.remove_block_entity_if_state(pos, stone));
1523        assert_eq!(proto.pending_block_entity_positions().len(), 0);
1524    }
1525
1526    #[test]
1527    fn dummy_factory_outcomes_keep_proto_and_full_stage_semantics() {
1528        init_vanilla_registry();
1529        init_behaviors();
1530        let proto = Chunk::new(
1531            Sections::from_owned(vec![ChunkSection::new_empty()].into_boxed_slice()),
1532            ChunkPos::new(0, 0),
1533            0,
1534            16,
1535            Weak::new(),
1536        );
1537        let moving_pos = BlockPos::new(2, 4, 5);
1538        let chest_pos = BlockPos::new(3, 4, 5);
1539        assert!(
1540            proto
1541                .set_block_state_for_generation(
1542                    ChunkStatus::Empty,
1543                    moving_pos,
1544                    vanilla_blocks::MOVING_PISTON.default_state(),
1545                    UpdateFlags::UPDATE_NONE,
1546                )
1547                .is_some()
1548        );
1549        assert!(
1550            proto
1551                .set_block_state_for_generation(
1552                    ChunkStatus::Empty,
1553                    chest_pos,
1554                    vanilla_blocks::CHEST.default_state(),
1555                    UpdateFlags::UPDATE_NONE,
1556                )
1557                .is_some()
1558        );
1559        proto.set_pending_block_entity(moving_pos);
1560        proto.set_pending_block_entity(chest_pos);
1561
1562        assert!(proto.promote_pending_block_entity(moving_pos).is_none());
1563        assert!(proto.promote_pending_block_entity(chest_pos).is_none());
1564        let pending = proto.pending_block_entity_positions();
1565        assert!(pending.contains(&moving_pos));
1566        assert!(pending.contains(&chest_pos));
1567
1568        let full = proto.promote_to_full().chunk;
1569        assert!(full.get_block_entity(moving_pos).is_none());
1570        assert!(!full.pending_block_entity_positions().contains(&moving_pos));
1571        assert!(full.get_block_entity(chest_pos).is_none());
1572        assert!(full.pending_block_entity_positions().contains(&chest_pos));
1573    }
1574
1575    #[test]
1576    fn stale_proto_factory_cannot_consume_a_replacement_marker() {
1577        init_vanilla_registry();
1578        init_behaviors();
1579        let proto = Chunk::new(
1580            Sections::from_owned(vec![ChunkSection::new_empty()].into_boxed_slice()),
1581            ChunkPos::new(0, 0),
1582            0,
1583            16,
1584            Weak::new(),
1585        );
1586        let pos = BlockPos::new(2, 4, 5);
1587        let copper = vanilla_blocks::COPPER_CHEST.default_state();
1588        let exposed = vanilla_blocks::EXPOSED_COPPER_CHEST.default_state();
1589        assert!(
1590            proto
1591                .set_block_state_for_generation(
1592                    ChunkStatus::Empty,
1593                    pos,
1594                    copper,
1595                    UpdateFlags::UPDATE_NONE,
1596                )
1597                .is_some()
1598        );
1599        proto.set_pending_block_entity(pos);
1600        let stale_entity: SharedBlockEntity = Arc::new(UnimplementedBlockEntity::new(
1601            &vanilla_block_entity_types::CHEST,
1602            Weak::new(),
1603            pos,
1604            copper,
1605        ));
1606
1607        assert_eq!(
1608            proto.set_block_state_for_generation(
1609                ChunkStatus::Empty,
1610                pos,
1611                exposed,
1612                UpdateFlags::UPDATE_NONE,
1613            ),
1614            Some(copper)
1615        );
1616        assert!(matches!(
1617            proto.commit_pending_creation(pos, copper, BlockEntityCreation::Created(stale_entity),),
1618            PendingPromotionCommit::Retry
1619        ));
1620        assert_eq!(proto.pending_block_entity_positions(), [pos]);
1621        assert!(proto.get_block_entity(pos).is_none());
1622    }
1623}