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