Skip to main content

steel_core/chunk/
section.rs

1//! This module contains the `Sections` and `ChunkSection` structs.
2use std::{
3    fmt::Debug,
4    io::Cursor,
5    ops::{Deref, DerefMut},
6    sync::{
7        Arc,
8        atomic::{AtomicU64, Ordering},
9    },
10};
11
12use parking_lot::{RwLockReadGuard, RwLockWriteGuard};
13use smallvec::SmallVec;
14use steel_registry::blocks::block_state_ext::BlockStateExt;
15use steel_registry::{REGISTRY, RegistryEntry};
16use steel_registry::{vanilla_biomes, vanilla_blocks};
17use steel_utils::{BlockPos, BlockStateId, ChunkPos, locks::SyncRwLock, serial::WriteTo};
18
19use crate::chunk::paletted_container::{BiomePalette, BlockPalette};
20use crate::physics::{
21    block_state_has_extensible_collision_behavior, block_state_may_expand_collision_cursor,
22};
23
24/// Lock-free index of sections containing randomly-ticking blocks or fluids.
25///
26/// Section writers update their bit while still holding the section lock. Readers
27/// use relaxed loads because this metadata only decides whether to attempt work;
28/// section contents remain protected by the section lock and brief staleness is
29/// acceptable in the same way as Vanilla's unsynchronized derived counters.
30#[derive(Debug)]
31pub(crate) struct RandomTickSectionBits {
32    words: Box<[AtomicU64]>,
33    section_count: usize,
34}
35
36impl RandomTickSectionBits {
37    fn new(section_count: usize) -> Self {
38        let word_count = section_count.div_ceil(u64::BITS as usize);
39        let words = (0..word_count)
40            .map(|_| AtomicU64::new(0))
41            .collect::<Vec<_>>()
42            .into_boxed_slice();
43        Self {
44            words,
45            section_count,
46        }
47    }
48
49    fn set(&self, section_index: usize, randomly_ticking: bool) {
50        debug_assert!(section_index < self.section_count);
51        let word_index = section_index / u64::BITS as usize;
52        let mask = 1_u64 << (section_index % u64::BITS as usize);
53        if randomly_ticking {
54            self.words[word_index].fetch_or(mask, Ordering::Relaxed);
55        } else {
56            self.words[word_index].fetch_and(!mask, Ordering::Relaxed);
57        }
58    }
59
60    fn contains(&self, section_index: usize) -> bool {
61        debug_assert!(section_index < self.section_count);
62        let word_index = section_index / u64::BITS as usize;
63        let mask = 1_u64 << (section_index % u64::BITS as usize);
64        self.words[word_index].load(Ordering::Relaxed) & mask != 0
65    }
66
67    /// Returns the next eligible section at or above `start`.
68    ///
69    /// Each call reloads the current word, so a random-tick callback changing a
70    /// later section can affect that later section in the same chunk pass.
71    #[must_use]
72    pub(crate) fn next(&self, start: usize) -> Option<usize> {
73        if start >= self.section_count {
74            return None;
75        }
76
77        let mut word_index = start / u64::BITS as usize;
78        let bit_index = start % u64::BITS as usize;
79        let mut bits = self.words[word_index].load(Ordering::Relaxed) & (u64::MAX << bit_index);
80        loop {
81            if bits != 0 {
82                let section_index =
83                    word_index * u64::BITS as usize + bits.trailing_zeros() as usize;
84                return (section_index < self.section_count).then_some(section_index);
85            }
86            word_index += 1;
87            let word = self.words.get(word_index)?;
88            bits = word.load(Ordering::Relaxed);
89        }
90    }
91
92    #[inline]
93    #[must_use]
94    pub(crate) fn is_empty(&self) -> bool {
95        self.next(0).is_none()
96    }
97}
98
99/// A wrapper around a chunk section.
100#[derive(Debug)]
101pub struct SectionHolder {
102    /// The chunk section data (requires lock to access).
103    section: SyncRwLock<ChunkSection>,
104    /// Shared lock-free random-tick section index.
105    randomly_ticking_sections: Arc<RandomTickSectionBits>,
106    section_index: usize,
107}
108
109impl SectionHolder {
110    /// Creates a new section holder.
111    #[must_use]
112    pub fn new(section: ChunkSection) -> Self {
113        let randomly_ticking_sections = Arc::new(RandomTickSectionBits::new(1));
114        Self::with_random_tick_index(section, randomly_ticking_sections, 0)
115    }
116
117    fn with_random_tick_index(
118        section: ChunkSection,
119        randomly_ticking_sections: Arc<RandomTickSectionBits>,
120        section_index: usize,
121    ) -> Self {
122        let randomly_ticking = section.is_randomly_ticking();
123        let result = Self {
124            section: SyncRwLock::new(section),
125            randomly_ticking_sections,
126            section_index,
127        };
128        if randomly_ticking {
129            result.randomly_ticking_sections.set(section_index, true);
130        }
131        result
132    }
133
134    /// Returns true if this section contains any randomly-ticking blocks or fluids.
135    ///
136    /// The mirror may briefly be stale relative to a concurrent section writer.
137    /// Section contents and the authoritative counter remain protected by the
138    /// section lock.
139    #[inline]
140    #[must_use]
141    pub fn is_randomly_ticking(&self) -> bool {
142        self.randomly_ticking_sections.contains(self.section_index)
143    }
144
145    /// Acquires a read lock on the section.
146    #[inline]
147    pub fn read(&self) -> RwLockReadGuard<'_, ChunkSection> {
148        self.section.read()
149    }
150
151    /// Attempts to acquire a read lock on the section.
152    #[inline]
153    pub fn try_read(&self) -> Option<RwLockReadGuard<'_, ChunkSection>> {
154        self.section.try_read()
155    }
156
157    /// Acquires a write lock on the section.
158    #[inline]
159    pub fn write(&self) -> SectionWriteGuard<'_> {
160        SectionWriteGuard::new(
161            self.section.write(),
162            &self.randomly_ticking_sections,
163            self.section_index,
164        )
165    }
166
167    /// Attempts to acquire a write lock on the section.
168    #[inline]
169    pub fn try_write(&self) -> Option<SectionWriteGuard<'_>> {
170        self.section.try_write().map(|guard| {
171            SectionWriteGuard::new(guard, &self.randomly_ticking_sections, self.section_index)
172        })
173    }
174}
175
176/// A chunk-section write guard that republishes derived lock-free metadata.
177pub struct SectionWriteGuard<'a> {
178    guard: RwLockWriteGuard<'a, ChunkSection>,
179    randomly_ticking_sections: &'a RandomTickSectionBits,
180    section_index: usize,
181    was_randomly_ticking: bool,
182}
183
184impl<'a> SectionWriteGuard<'a> {
185    fn new(
186        guard: RwLockWriteGuard<'a, ChunkSection>,
187        randomly_ticking_sections: &'a RandomTickSectionBits,
188        section_index: usize,
189    ) -> Self {
190        let was_randomly_ticking = guard.is_randomly_ticking();
191        Self {
192            guard,
193            randomly_ticking_sections,
194            section_index,
195            was_randomly_ticking,
196        }
197    }
198
199    /// Sets one block through the tracked mutation path.
200    ///
201    /// This updates section counters and stable-air occupancy without triggering the conservative
202    /// invalidation used for arbitrary mutable access.
203    pub fn set_block_state(
204        &mut self,
205        x: usize,
206        y: usize,
207        z: usize,
208        new_state: BlockStateId,
209    ) -> BlockStateId {
210        self.guard.set_block_state(x, y, z, new_state)
211    }
212
213    /// Sets one world-generation block while retaining a conservative exposure certificate.
214    pub(crate) fn set_block_state_for_generation(
215        &mut self,
216        x: usize,
217        y: usize,
218        z: usize,
219        new_state: BlockStateId,
220    ) -> BlockStateId {
221        self.guard
222            .set_block_state_for_generation(x, y, z, new_state)
223    }
224
225    /// Finalizes deferred world-generation state while keeping derived metadata synchronized.
226    pub(crate) fn finalize_generation_counts_if_needed(&mut self) {
227        self.guard.finalize_generation_counts_if_needed();
228    }
229
230    /// Rebuilds all section-derived metadata from the current block palette.
231    pub fn recalculate_counts(&mut self) {
232        self.guard.recalculate_counts();
233    }
234}
235
236impl Deref for SectionWriteGuard<'_> {
237    type Target = ChunkSection;
238
239    fn deref(&self) -> &Self::Target {
240        &self.guard
241    }
242}
243
244impl DerefMut for SectionWriteGuard<'_> {
245    fn deref_mut(&mut self) -> &mut Self::Target {
246        // Arbitrary mutable access can reach the block palette without going through a
247        // tracked setter. Keep the exposure certificate conservative until a recount rebuilds it.
248        self.guard.invalidate_stable_air_occupancy();
249        &mut self.guard
250    }
251}
252
253impl Drop for SectionWriteGuard<'_> {
254    fn drop(&mut self) {
255        let is_randomly_ticking = self.guard.is_randomly_ticking();
256        if is_randomly_ticking != self.was_randomly_ticking {
257            self.randomly_ticking_sections
258                .set(self.section_index, is_randomly_ticking);
259        }
260    }
261}
262
263/// A collection of chunk sections.
264#[derive(Debug)]
265pub struct Sections {
266    /// The sections in the collection.
267    pub sections: Box<[SectionHolder]>,
268    randomly_ticking_sections: Arc<RandomTickSectionBits>,
269}
270
271/// Cached section counter traits for one block state.
272#[derive(Clone, Copy, Debug, PartialEq, Eq)]
273pub(crate) struct BlockStateSectionCounts {
274    is_air: bool,
275    has_fluid: bool,
276    randomly_ticking_block: bool,
277    randomly_ticking_fluid: bool,
278}
279
280const BLOCKS_PER_SECTION: u16 = BlockPalette::VOLUME as u16;
281const STABLE_AIR_OCCUPANCY_ROW_COUNT: usize = BlockPalette::SIZE * BlockPalette::SIZE;
282
283/// Section-local occupancy of every state except Vanilla's three immutable air blocks.
284///
285/// `Unknown` is deliberately treated as occupied. It is used after arbitrary mutable palette
286/// access so an optimization can never hide plugin or future mutation paths that bypass the
287/// tracked setters.
288#[derive(Debug)]
289enum StableAirOccupancy {
290    Unknown,
291    Empty,
292    Full,
293    Mixed {
294        rows: Box<[u16; STABLE_AIR_OCCUPANCY_ROW_COUNT]>,
295        occupied_count: u16,
296    },
297}
298
299impl StableAirOccupancy {
300    fn from_palette(states: &BlockPalette) -> Self {
301        let data = match states {
302            BlockPalette::Homogeneous(state) if is_stable_vanilla_air(*state) => {
303                return Self::Empty;
304            }
305            BlockPalette::Homogeneous(_) => return Self::Full,
306            BlockPalette::Heterogeneous(data) => data,
307            BlockPalette::Building(_) => return Self::Unknown,
308        };
309        // Classify each palette entry once instead of every block position.
310        let mut air_states = SmallVec::<[BlockStateId; 3]>::new();
311        let mut occupied_count = 0_u16;
312        for &(state, count) in data.palette() {
313            if is_stable_vanilla_air(state) {
314                air_states.push(state);
315            } else {
316                occupied_count += count;
317            }
318        }
319        if air_states.is_empty() {
320            return Self::Full;
321        }
322        if occupied_count == 0 {
323            return Self::Empty;
324        }
325
326        let mut rows = Box::new([0; STABLE_AIR_OCCUPANCY_ROW_COUNT]);
327        // Palette cubes iterate in Y, Z, X order, so each run of 16 values is one row.
328        for (index, state) in data.iter_values().enumerate() {
329            if !air_states.contains(state) {
330                rows[index / BlockPalette::SIZE] |= 1_u16 << (index % BlockPalette::SIZE);
331            }
332        }
333        debug_assert!(occupied_count < BLOCKS_PER_SECTION);
334        Self::Mixed {
335            rows,
336            occupied_count,
337        }
338    }
339
340    fn record_change(
341        &mut self,
342        x: usize,
343        y: usize,
344        z: usize,
345        old_state: BlockStateId,
346        new_state: BlockStateId,
347    ) {
348        let old_occupied = !is_stable_vanilla_air(old_state);
349        let new_occupied = !is_stable_vanilla_air(new_state);
350        if old_occupied == new_occupied {
351            return;
352        }
353
354        let row_index = y * BlockPalette::SIZE + z;
355        let bit = 1_u16 << x;
356        match self {
357            Self::Unknown => {}
358            Self::Empty => {
359                debug_assert!(new_occupied);
360                let mut rows = Box::new([0; STABLE_AIR_OCCUPANCY_ROW_COUNT]);
361                rows[row_index] = bit;
362                *self = Self::Mixed {
363                    rows,
364                    occupied_count: 1,
365                };
366            }
367            Self::Full => {
368                debug_assert!(old_occupied);
369                let mut rows = Box::new([u16::MAX; STABLE_AIR_OCCUPANCY_ROW_COUNT]);
370                rows[row_index] &= !bit;
371                *self = Self::Mixed {
372                    rows,
373                    occupied_count: BLOCKS_PER_SECTION - 1,
374                };
375            }
376            Self::Mixed {
377                rows,
378                occupied_count,
379            } => {
380                if new_occupied {
381                    rows[row_index] |= bit;
382                    *occupied_count += 1;
383                    if *occupied_count == BLOCKS_PER_SECTION {
384                        *self = Self::Full;
385                    }
386                } else {
387                    rows[row_index] &= !bit;
388                    *occupied_count -= 1;
389                    if *occupied_count == 0 {
390                        *self = Self::Empty;
391                    }
392                }
393            }
394        }
395    }
396
397    fn box_is_clear(
398        &self,
399        min_x: usize,
400        max_x: usize,
401        min_y: usize,
402        max_y: usize,
403        min_z: usize,
404        max_z: usize,
405    ) -> bool {
406        debug_assert!(min_x <= max_x && max_x < BlockPalette::SIZE);
407        debug_assert!(min_y <= max_y && max_y < BlockPalette::SIZE);
408        debug_assert!(min_z <= max_z && max_z < BlockPalette::SIZE);
409
410        let Self::Mixed { rows, .. } = self else {
411            return matches!(self, Self::Empty);
412        };
413        let below_max = if max_x == u16::BITS as usize - 1 {
414            u16::MAX
415        } else {
416            (1_u16 << (max_x + 1)) - 1
417        };
418        let x_mask = below_max & (u16::MAX << min_x);
419        for y in min_y..=max_y {
420            for z in min_z..=max_z {
421                if rows[y * BlockPalette::SIZE + z] & x_mask != 0 {
422                    return false;
423                }
424            }
425        }
426        true
427    }
428}
429
430fn is_stable_vanilla_air(state: BlockStateId) -> bool {
431    // The cached air flag avoids three registry lookups for the common non-air state.
432    state.is_air()
433        && (state == vanilla_blocks::AIR.default_state()
434            || state == vanilla_blocks::CAVE_AIR.default_state()
435            || state == vanilla_blocks::VOID_AIR.default_state())
436}
437
438impl Sections {
439    /// Creates a new `Sections` from a box of owned `ChunkSection`s.
440    #[must_use]
441    pub fn from_owned(sections: Box<[ChunkSection]>) -> Self {
442        let randomly_ticking_sections = Arc::new(RandomTickSectionBits::new(sections.len()));
443        let holders: Box<[SectionHolder]> = sections
444            .into_vec()
445            .into_iter()
446            .enumerate()
447            .map(|(section_index, section)| {
448                SectionHolder::with_random_tick_index(
449                    section,
450                    Arc::clone(&randomly_ticking_sections),
451                    section_index,
452                )
453            })
454            .collect();
455        Self {
456            sections: holders,
457            randomly_ticking_sections,
458        }
459    }
460
461    /// Returns the shared lock-free random-tick section index.
462    #[must_use]
463    pub(crate) const fn random_tick_sections(&self) -> &Arc<RandomTickSectionBits> {
464        &self.randomly_ticking_sections
465    }
466
467    /// Gets a block at a relative position in the chunk.
468    #[must_use]
469    pub fn get_relative_block(
470        &self,
471        relative_x: usize,
472        relative_y: usize,
473        relative_z: usize,
474    ) -> Option<BlockStateId> {
475        debug_assert!(relative_x < BlockPalette::SIZE);
476        debug_assert!(relative_z < BlockPalette::SIZE);
477
478        let section_index = relative_y / BlockPalette::SIZE;
479        let relative_y = relative_y % BlockPalette::SIZE;
480        self.sections.get(section_index).map(|section| {
481            section
482                .read()
483                .states
484                .get(relative_x, relative_y, relative_z)
485        })
486    }
487
488    /// Reads an entire column at `(x, z)` across all sections into a caller-owned buffer.
489    ///
490    /// Holds each section's read lock once for 16 Y reads instead of acquiring
491    /// a lock per block. Indexed by `relative_y` (0 = chunk min-y).
492    /// The buffer is resized if needed and reused across calls to avoid allocation.
493    pub fn read_column_into(&self, x: usize, z: usize, buf: &mut Vec<BlockStateId>) {
494        debug_assert!(x < BlockPalette::SIZE);
495        debug_assert!(z < BlockPalette::SIZE);
496
497        let total = self.sections.len() * 16;
498        if buf.len() != total {
499            buf.resize(total, BlockStateId::default());
500        }
501        for (i, holder) in self.sections.iter().enumerate() {
502            let guard = holder.read();
503            let base = i * 16;
504            guard
505                .states
506                .copy_column_into(x, z, &mut buf[base..base + 16]);
507        }
508    }
509
510    /// Reads all biome palette values into a flat array.
511    ///
512    /// Indexed as `[section_idx * 64 + qy * 16 + qz * 4 + qx]`.
513    /// Holds each section's read lock once for all 64 biome reads.
514    #[must_use]
515    pub fn read_all_biomes(&self) -> Box<[u16]> {
516        let total = self.sections.len() * 64;
517        let mut biomes = vec![0u16; total];
518        for (i, holder) in self.sections.iter().enumerate() {
519            let guard = holder.read();
520            let base = i * 64;
521            for qy in 0..4 {
522                for qz in 0..4 {
523                    for qx in 0..4 {
524                        biomes[base + qy * 16 + qz * 4 + qx] = guard.biomes.get(qx, qy, qz);
525                    }
526                }
527            }
528        }
529        biomes.into_boxed_slice()
530    }
531
532    /// Visits every biome palette value in section order while holding each
533    /// section's read lock once.
534    pub fn for_each_biome_id(&self, mut visitor: impl FnMut(u16)) {
535        for holder in &self.sections {
536            let guard = holder.read();
537            for qy in 0..4 {
538                for qz in 0..4 {
539                    for qx in 0..4 {
540                        visitor(guard.biomes.get(qx, qy, qz));
541                    }
542                }
543            }
544        }
545    }
546
547    /// Returns whether each real chunk section contains no non-air blocks.
548    #[must_use]
549    pub fn section_emptiness_map(&self) -> Box<[bool]> {
550        self.sections
551            .iter()
552            .map(|section| section.read().is_empty())
553            .collect()
554    }
555
556    /// Returns block-light source positions in `ScalableLux` section/local-index order.
557    #[must_use]
558    pub fn block_light_sources(&self, chunk_pos: ChunkPos, min_y: i32) -> Vec<BlockPos> {
559        let mut sources = Vec::new();
560        let chunk_min_x = chunk_pos.0.x * BlockPalette::SIZE as i32;
561        let chunk_min_z = chunk_pos.0.y * BlockPalette::SIZE as i32;
562
563        for (section_index, section) in self.sections.iter().enumerate() {
564            let section_min_y = min_y + (section_index * BlockPalette::SIZE) as i32;
565            section.read().append_block_light_sources(
566                chunk_min_x,
567                section_min_y,
568                chunk_min_z,
569                &mut sources,
570            );
571        }
572
573        sources
574    }
575
576    /// Writes multiple blocks in one column, holding each section's write guard
577    /// across all writes to that section. Most efficient when blocks are grouped
578    /// by section (e.g. descending `relative_y` from a top-to-bottom scan).
579    pub fn write_column_blocks(&self, x: usize, z: usize, blocks: &[(usize, BlockStateId)]) {
580        const DIM: usize = BlockPalette::SIZE;
581        debug_assert!(x < DIM);
582        debug_assert!(z < DIM);
583
584        let mut i = 0;
585        while i < blocks.len() {
586            let section_idx = blocks[i].0 / DIM;
587            let mut guard = self.sections[section_idx].write();
588            guard.states.enter_building_mode();
589            let Some(cube) = guard.states.as_building_slice_mut() else {
590                unreachable!("just entered building mode")
591            };
592            let xz_base = z * DIM + x;
593            while i < blocks.len() && blocks[i].0 / DIM == section_idx {
594                let (rel_y, value) = blocks[i];
595                let local_y = rel_y % DIM;
596                cube[local_y * DIM * DIM + xz_base] = value;
597                i += 1;
598            }
599        }
600    }
601
602    /// Writes a batch of blocks at arbitrary positions, holding each section's
603    /// write guard across consecutive entries in the same section. Blocks should
604    /// be roughly grouped by section index for best performance.
605    ///
606    /// Each touched section enters worldgen Building mode (raw cube, no palette
607    /// tracking) so writes are O(1) stores. Per-write goes through a flat
608    /// `&mut [V]` view of the cube — bypasses the 3-arm `set` match and the
609    /// unused old-value load. `recalculate_counts` finalizes.
610    pub fn write_block_batch(&self, blocks: &[(usize, usize, usize, BlockStateId)]) {
611        const DIM: usize = BlockPalette::SIZE;
612        let mut i = 0;
613        while i < blocks.len() {
614            let section_idx = blocks[i].1 / DIM;
615            let mut guard = self.sections[section_idx].write();
616            guard.states.enter_building_mode();
617            let Some(cube) = guard.states.as_building_slice_mut() else {
618                // enter_building_mode just transitioned to Building.
619                unreachable!("just entered building mode")
620            };
621            while i < blocks.len() && blocks[i].1 / DIM == section_idx {
622                let (x, rel_y, z, value) = blocks[i];
623                let local_y = rel_y % DIM;
624                cube[local_y * DIM * DIM + z * DIM + x] = value;
625                i += 1;
626            }
627        }
628    }
629
630    /// Writes a batch of blocks while maintaining section counters and palette state.
631    ///
632    /// Blocks should be grouped by section index so each touched section only needs
633    /// one write guard.
634    pub(crate) fn write_tracked_block_batch(&self, blocks: &[(usize, usize, usize, BlockStateId)]) {
635        const DIM: usize = BlockPalette::SIZE;
636        let mut i = 0;
637        while i < blocks.len() {
638            let section_idx = blocks[i].1 / DIM;
639            let mut guard = self.sections[section_idx].write();
640            while i < blocks.len() && blocks[i].1 / DIM == section_idx {
641                let (x, relative_y, z, value) = blocks[i];
642                guard.set_block_state(x, relative_y % DIM, z, value);
643                i += 1;
644            }
645        }
646    }
647
648    /// Sets a block at a relative position in the chunk and keeps section
649    /// counters/palette serialization ready.
650    pub fn set_relative_block(
651        &self,
652        relative_x: usize,
653        relative_y: usize,
654        relative_z: usize,
655        value: BlockStateId,
656    ) {
657        debug_assert!(relative_x < BlockPalette::SIZE);
658        debug_assert!(relative_z < BlockPalette::SIZE);
659
660        let idx = relative_y / BlockPalette::SIZE;
661        let relative_y = relative_y % BlockPalette::SIZE;
662        let mut guard = self.sections[idx].write();
663        guard.set_block_state(relative_x, relative_y, relative_z, value);
664    }
665
666    /// Sets a block during worldgen using the raw building palette path.
667    ///
668    /// Callers must finalize by recounting touched sections before save,
669    /// promotion, or packet serialization.
670    pub(crate) fn set_relative_block_for_generation(
671        &self,
672        relative_x: usize,
673        relative_y: usize,
674        relative_z: usize,
675        value: BlockStateId,
676    ) {
677        debug_assert!(relative_x < BlockPalette::SIZE);
678        debug_assert!(relative_z < BlockPalette::SIZE);
679
680        let idx = relative_y / BlockPalette::SIZE;
681        let relative_y = relative_y % BlockPalette::SIZE;
682        let mut guard = self.sections[idx].write();
683        guard.set_block_state_for_generation(relative_x, relative_y, relative_z, value);
684    }
685}
686
687/// A chunk section.
688///
689/// Contains a 16x16x16 cube of block states and biomes, along with cached
690/// counts for optimization (similar to vanilla's `LevelChunkSection`).
691#[derive(Debug)]
692pub struct ChunkSection {
693    /// The block states in the section.
694    states: BlockPalette,
695    /// The biomes in the section.
696    pub biomes: BiomePalette,
697    /// Number of non-air blocks in this section (0-4096).
698    /// Used to quickly check if a section is empty.
699    non_empty_block_count: u16,
700    /// Number of fluid-containing blocks in this section (0-4096).
701    /// Includes water, lava, and waterlogged blocks.
702    fluid_count: u16,
703    /// Number of randomly-ticking blocks in this section (0-4096).
704    pub ticking_block_count: u16,
705    /// Number of randomly-ticking fluids in this section (0-4096).
706    ticking_fluid_count: u16,
707    /// Conservative spatial certificate for immutable Vanilla air.
708    stable_air_occupancy: StableAirOccupancy,
709}
710
711impl ChunkSection {
712    /// Creates a new chunk section with the given block states and biomes.
713    ///
714    /// Note: You must call `recalculate_counts()` after creation to initialize
715    /// the cached counters if the states palette contains non-air blocks.
716    #[must_use]
717    pub const fn new_with_biomes(states: BlockPalette, biomes: BiomePalette) -> Self {
718        Self {
719            states,
720            biomes,
721            non_empty_block_count: 0,
722            fluid_count: 0,
723            ticking_block_count: 0,
724            ticking_fluid_count: 0,
725            stable_air_occupancy: StableAirOccupancy::Unknown,
726        }
727    }
728
729    /// Creates a new empty chunk section.
730    #[must_use]
731    pub fn new_empty() -> Self {
732        let plains_id = vanilla_biomes::PLAINS.id() as u16;
733        Self {
734            states: BlockPalette::Homogeneous(BlockStateId(0)),
735            biomes: BiomePalette::Homogeneous(plains_id),
736            non_empty_block_count: 0,
737            fluid_count: 0,
738            ticking_block_count: 0,
739            ticking_fluid_count: 0,
740            stable_air_occupancy: StableAirOccupancy::Empty,
741        }
742    }
743
744    /// Returns the section's read-only block palette.
745    #[must_use]
746    pub(crate) const fn states(&self) -> &BlockPalette {
747        &self.states
748    }
749
750    /// Returns true if this section contains no non-air blocks.
751    #[must_use]
752    pub const fn is_empty(&self) -> bool {
753        self.non_empty_block_count == 0
754    }
755
756    /// Returns true if this section contains any randomly-ticking blocks or fluids.
757    #[must_use]
758    pub const fn is_randomly_ticking(&self) -> bool {
759        self.is_randomly_ticking_blocks() || self.is_randomly_ticking_fluids()
760    }
761
762    /// Returns true if this section contains any randomly-ticking blocks.
763    #[must_use]
764    pub const fn is_randomly_ticking_blocks(&self) -> bool {
765        self.ticking_block_count > 0
766    }
767
768    /// Returns true if this section contains any randomly-ticking fluids.
769    #[must_use]
770    pub const fn is_randomly_ticking_fluids(&self) -> bool {
771        self.ticking_fluid_count > 0
772    }
773
774    /// Returns true if this section's palette may contain block-light sources.
775    #[must_use]
776    pub fn maybe_has_block_light_sources(&self) -> bool {
777        !self.is_empty()
778            && self
779                .states
780                .maybe_has(|state| state.get_light_emission() > 0)
781    }
782
783    /// Returns whether this section may need Vanilla's expanded collision cursor boundary.
784    ///
785    /// Dynamic Vanilla states have no cached collision shape, so Vanilla treats all of them as
786    /// potentially large. Plugin-owned states receive the same conservative treatment because
787    /// their behavior may supply a collision shape that is not represented by extracted data.
788    #[must_use]
789    pub(crate) fn maybe_has_special_colliding_blocks(&self) -> bool {
790        self.states
791            .maybe_has(block_state_may_expand_collision_cursor)
792    }
793
794    /// Returns whether collision behavior calls for this section must observe live block states.
795    ///
796    /// Steel owns and audits the frozen `minecraft` behavior table. Plugin-owned block behavior
797    /// may mutate other positions while resolving a shape, so those sections cannot use a block
798    /// state snapshot across callbacks.
799    #[must_use]
800    pub(crate) fn maybe_has_extensible_collision_behavior(&self) -> bool {
801        self.states
802            .maybe_has(block_state_has_extensible_collision_behavior)
803    }
804
805    /// Returns whether every block in one inclusive local box is immutable Vanilla air.
806    #[must_use]
807    pub(crate) fn stable_air_box_is_clear(
808        &self,
809        min_x: usize,
810        max_x: usize,
811        min_y: usize,
812        max_y: usize,
813        min_z: usize,
814        max_z: usize,
815    ) -> bool {
816        self.stable_air_occupancy
817            .box_is_clear(min_x, max_x, min_y, max_y, min_z, max_z)
818    }
819
820    fn invalidate_stable_air_occupancy(&mut self) {
821        self.stable_air_occupancy = StableAirOccupancy::Unknown;
822    }
823
824    /// Appends block-light source positions in `ScalableLux` local-index order.
825    pub fn append_block_light_sources(
826        &self,
827        chunk_min_x: i32,
828        section_min_y: i32,
829        chunk_min_z: i32,
830        sources: &mut Vec<BlockPos>,
831    ) {
832        if !self.maybe_has_block_light_sources() {
833            return;
834        }
835
836        for local_index in 0..BlockPalette::VOLUME {
837            let state = self.states.get_at_index(local_index);
838            if state.get_light_emission() == 0 {
839                continue;
840            }
841
842            sources.push(BlockPos::new(
843                chunk_min_x + (local_index & 15) as i32,
844                section_min_y + (local_index >> 8) as i32,
845                chunk_min_z + ((local_index >> 4) & 15) as i32,
846            ));
847        }
848    }
849
850    /// Returns the number of non-air blocks in this section.
851    #[must_use]
852    pub const fn non_empty_block_count(&self) -> u16 {
853        self.non_empty_block_count
854    }
855
856    /// Returns the number of fluid-containing blocks in this section.
857    #[must_use]
858    pub const fn fluid_count(&self) -> u16 {
859        self.fluid_count
860    }
861
862    /// Returns if the chunk has fluid.
863    #[must_use]
864    pub const fn has_fluid(&self) -> bool {
865        self.fluid_count > 0
866    }
867
868    /// Returns the number of randomly-ticking blocks in this section.
869    #[must_use]
870    pub const fn ticking_block_count(&self) -> u16 {
871        self.ticking_block_count
872    }
873
874    /// Returns the number of randomly-ticking fluids in this section.
875    #[must_use]
876    pub const fn ticking_fluid_count(&self) -> u16 {
877        self.ticking_fluid_count
878    }
879
880    /// Recalculates cached counters from extracted per-state metadata.
881    ///
882    /// Iterates the palette (`O(palette_size)`) rather than every cube cell
883    /// (`O(4096)`): each block-state appears at most once in the palette and
884    /// carries its own occurrence count, so we just classify each unique state
885    /// and multiply by its count. Mirrors Moonrise's `BlockCountingBitStorage`.
886    /// For a `Homogeneous` section that's a single classify; for typical
887    /// `Heterogeneous` sections palette is well under 16 entries.
888    pub fn recalculate_counts(&mut self) {
889        self.recalculate_counts_from_palette(Self::block_state_section_counts);
890    }
891
892    fn recalculate_counts_from_palette(
893        &mut self,
894        mut counts_for_state: impl FnMut(BlockStateId) -> BlockStateSectionCounts,
895    ) {
896        self.states.finalize_building();
897        let stable_air_occupancy = StableAirOccupancy::from_palette(&self.states);
898
899        let mut non_empty: u16 = 0;
900        let mut fluid: u16 = 0;
901        let mut ticking_blocks: u16 = 0;
902        let mut ticking_fluids: u16 = 0;
903
904        match &self.states {
905            BlockPalette::Homogeneous(state) => {
906                let counts = counts_for_state(*state);
907                Self::accumulate_counter_traits(
908                    &mut non_empty,
909                    &mut fluid,
910                    &mut ticking_blocks,
911                    &mut ticking_fluids,
912                    counts,
913                    BLOCKS_PER_SECTION,
914                );
915            }
916            BlockPalette::Heterogeneous(data) => {
917                for &(state, count) in data.palette() {
918                    let counts = counts_for_state(state);
919                    Self::accumulate_counter_traits(
920                        &mut non_empty,
921                        &mut fluid,
922                        &mut ticking_blocks,
923                        &mut ticking_fluids,
924                        counts,
925                        count,
926                    );
927                }
928            }
929            BlockPalette::Building(_) => unreachable!("finalize_building was just called"),
930        }
931
932        self.non_empty_block_count = non_empty;
933        self.fluid_count = fluid;
934        self.ticking_block_count = ticking_blocks;
935        self.ticking_fluid_count = ticking_fluids;
936        self.stable_air_occupancy = stable_air_occupancy;
937    }
938
939    const fn accumulate_counter_traits(
940        non_empty: &mut u16,
941        fluid: &mut u16,
942        ticking_blocks: &mut u16,
943        ticking_fluids: &mut u16,
944        counts: BlockStateSectionCounts,
945        block_count: u16,
946    ) {
947        if !counts.is_air {
948            *non_empty += block_count;
949        }
950        if counts.has_fluid {
951            *fluid += block_count;
952        }
953        if counts.randomly_ticking_block {
954            *ticking_blocks += block_count;
955        }
956        if counts.randomly_ticking_fluid {
957            *ticking_fluids += block_count;
958        }
959    }
960
961    /// Whether this section's palette contains any POI-type block state.
962    ///
963    /// Lets the Full-stage POI populate skip the full 4096-block
964    /// `scan_and_populate` for the overwhelming majority of sections
965    /// (stone/dirt/air) that hold no POI blocks — a palette scan of `O(≤16)`
966    /// instead of `O(4096)`. Mirrors vanilla's `LevelChunkSection.maybeHas`.
967    #[must_use]
968    pub fn contains_poi(&self) -> bool {
969        let poi = &REGISTRY.poi_types;
970        match &self.states {
971            BlockPalette::Homogeneous(state) => poi.is_poi_state(*state),
972            BlockPalette::Heterogeneous(data) => data
973                .palette()
974                .iter()
975                .any(|(state, _)| poi.is_poi_state(*state)),
976            // Not yet finalized (only happens mid-worldgen, not at promotion);
977            // fall back to scanning rather than risk missing a POI.
978            BlockPalette::Building(_) => true,
979        }
980    }
981
982    /// Sets a block state and updates the cached counters.
983    ///
984    /// Returns the old block state.
985    ///
986    pub fn set_block_state(
987        &mut self,
988        x: usize,
989        y: usize,
990        z: usize,
991        new_state: BlockStateId,
992    ) -> BlockStateId {
993        self.ensure_counter_ready_for_delta();
994        let old_state = self.states.set(x, y, z, new_state);
995
996        if old_state != new_state {
997            let old_counts = Self::block_state_section_counts(old_state);
998            let new_counts = Self::block_state_section_counts(new_state);
999            self.apply_count_change(old_counts, new_counts);
1000            self.stable_air_occupancy
1001                .record_change(x, y, z, old_state, new_state);
1002        }
1003
1004        old_state
1005    }
1006
1007    /// Sets a block state through the raw worldgen building path.
1008    ///
1009    /// Returns the old block state. Cached counters are intentionally not
1010    /// updated; callers must recount before light, promotion, save, or packet
1011    /// serialization.
1012    pub(crate) fn set_block_state_for_generation(
1013        &mut self,
1014        x: usize,
1015        y: usize,
1016        z: usize,
1017        new_state: BlockStateId,
1018    ) -> BlockStateId {
1019        self.invalidate_stable_air_occupancy();
1020        self.states.enter_building_mode();
1021        self.states.set(x, y, z, new_state)
1022    }
1023
1024    /// Returns the cached-counter traits for a block state.
1025    pub(crate) fn block_state_section_counts(state: BlockStateId) -> BlockStateSectionCounts {
1026        let metadata = state.get_ticking_metadata();
1027        BlockStateSectionCounts {
1028            is_air: metadata.is_air(),
1029            has_fluid: metadata.has_fluid(),
1030            randomly_ticking_block: metadata.randomly_ticking_block(),
1031            randomly_ticking_fluid: metadata.randomly_ticking_fluid(),
1032        }
1033    }
1034
1035    pub(crate) fn finalize_generation_counts_if_needed(&mut self) {
1036        if matches!(&self.states, BlockPalette::Building(_)) {
1037            self.recalculate_counts();
1038        }
1039    }
1040
1041    fn ensure_counter_ready_for_delta(&mut self) {
1042        if matches!(&self.states, BlockPalette::Building(_)) {
1043            log::debug!(
1044                "finalizing worldgen Building palette before applying a counter-aware \
1045                 block-state delta"
1046            );
1047            self.recalculate_counts();
1048        }
1049    }
1050
1051    const fn apply_count_change(
1052        &mut self,
1053        old_counts: BlockStateSectionCounts,
1054        new_counts: BlockStateSectionCounts,
1055    ) {
1056        if !old_counts.is_air && new_counts.is_air {
1057            self.non_empty_block_count -= 1;
1058        } else if old_counts.is_air && !new_counts.is_air {
1059            self.non_empty_block_count += 1;
1060        }
1061
1062        if old_counts.has_fluid && !new_counts.has_fluid {
1063            self.fluid_count -= 1;
1064        } else if !old_counts.has_fluid && new_counts.has_fluid {
1065            self.fluid_count += 1;
1066        }
1067
1068        if old_counts.randomly_ticking_block && !new_counts.randomly_ticking_block {
1069            self.ticking_block_count -= 1;
1070        } else if !old_counts.randomly_ticking_block && new_counts.randomly_ticking_block {
1071            self.ticking_block_count += 1;
1072        }
1073
1074        if old_counts.randomly_ticking_fluid && !new_counts.randomly_ticking_fluid {
1075            self.ticking_fluid_count -= 1;
1076        } else if !old_counts.randomly_ticking_fluid && new_counts.randomly_ticking_fluid {
1077            self.ticking_fluid_count += 1;
1078        }
1079    }
1080
1081    /// Writes the chunk section to a writer.
1082    ///
1083    /// # Panics
1084    /// - If the writer fails to write.
1085    pub fn write(&self, writer: &mut Cursor<Vec<u8>>) {
1086        self.non_empty_block_count
1087            .write(writer)
1088            .expect("Failed to write block count");
1089        self.fluid_count
1090            .write(writer)
1091            .expect("Failed to write fluid count");
1092
1093        self.states
1094            .write(writer)
1095            .expect("Failed to write block states");
1096        self.biomes.write(writer).expect("Failed to write biomes");
1097    }
1098}
1099
1100#[cfg(test)]
1101mod tests {
1102    use steel_registry::init_vanilla_registry;
1103    use steel_registry::vanilla_blocks;
1104
1105    use crate::behavior::init_behaviors;
1106
1107    use super::*;
1108
1109    const SECTION_MAX_LOCAL_COORDINATE: usize = BlockPalette::SIZE - 1;
1110
1111    fn plains_biomes() -> BiomePalette {
1112        BiomePalette::Homogeneous(vanilla_biomes::PLAINS.id() as u16)
1113    }
1114
1115    fn init_test_behaviors() {
1116        init_vanilla_registry();
1117        init_behaviors();
1118    }
1119
1120    fn entire_section_is_stable_air(section: &ChunkSection) -> bool {
1121        section.stable_air_box_is_clear(
1122            0,
1123            SECTION_MAX_LOCAL_COORDINATE,
1124            0,
1125            SECTION_MAX_LOCAL_COORDINATE,
1126            0,
1127            SECTION_MAX_LOCAL_COORDINATE,
1128        )
1129    }
1130
1131    #[test]
1132    fn stable_air_occupancy_tracks_canonical_air_and_non_air_transitions() {
1133        init_test_behaviors();
1134        let mut section = ChunkSection::new_empty();
1135
1136        assert!(entire_section_is_stable_air(&section));
1137        section.set_block_state(2, 3, 4, vanilla_blocks::CAVE_AIR.default_state());
1138        section.set_block_state(5, 6, 7, vanilla_blocks::VOID_AIR.default_state());
1139        assert!(entire_section_is_stable_air(&section));
1140
1141        section.set_block_state(2, 3, 4, vanilla_blocks::STONE.default_state());
1142        assert!(!section.stable_air_box_is_clear(2, 2, 3, 3, 4, 4));
1143        assert!(section.stable_air_box_is_clear(3, 3, 3, 3, 4, 4));
1144        section.set_block_state(2, 3, 4, vanilla_blocks::AIR.default_state());
1145        assert!(entire_section_is_stable_air(&section));
1146
1147        let mut full = ChunkSection::new_with_biomes(
1148            BlockPalette::Homogeneous(vanilla_blocks::STONE.default_state()),
1149            plains_biomes(),
1150        );
1151        full.recalculate_counts();
1152        assert!(!full.stable_air_box_is_clear(0, 0, 0, 0, 0, 0));
1153        full.set_block_state(0, 0, 0, vanilla_blocks::AIR.default_state());
1154        assert!(full.stable_air_box_is_clear(0, 0, 0, 0, 0, 0));
1155        assert!(!full.stable_air_box_is_clear(1, 1, 0, 0, 0, 0));
1156        full.set_block_state(0, 0, 0, vanilla_blocks::STONE.default_state());
1157        assert!(!full.stable_air_box_is_clear(0, 0, 0, 0, 0, 0));
1158    }
1159
1160    #[test]
1161    fn arbitrary_palette_mutation_invalidates_stable_air_certificate_until_recount() {
1162        init_test_behaviors();
1163        let holder = SectionHolder::new(ChunkSection::new_empty());
1164        {
1165            let mut guard = holder.write();
1166            guard.states.enter_building_mode();
1167        }
1168        assert!(!entire_section_is_stable_air(&holder.read()));
1169
1170        holder.write().recalculate_counts();
1171        assert!(entire_section_is_stable_air(&holder.read()));
1172    }
1173
1174    #[test]
1175    fn write_guard_setter_keeps_stable_air_occupancy_tracked() {
1176        const LOCAL_X: usize = 2;
1177        const LOCAL_Y: usize = 3;
1178        const LOCAL_Z: usize = 4;
1179
1180        init_test_behaviors();
1181        let holder = SectionHolder::new(ChunkSection::new_empty());
1182
1183        holder.write().set_block_state(
1184            LOCAL_X,
1185            LOCAL_Y,
1186            LOCAL_Z,
1187            vanilla_blocks::STONE.default_state(),
1188        );
1189        assert!(
1190            !holder
1191                .read()
1192                .stable_air_box_is_clear(LOCAL_X, LOCAL_X, LOCAL_Y, LOCAL_Y, LOCAL_Z, LOCAL_Z,)
1193        );
1194
1195        holder.write().set_block_state(
1196            LOCAL_X,
1197            LOCAL_Y,
1198            LOCAL_Z,
1199            vanilla_blocks::AIR.default_state(),
1200        );
1201        assert!(entire_section_is_stable_air(&holder.read()));
1202    }
1203
1204    #[test]
1205    fn raw_bulk_writes_invalidate_stable_air_certificate_until_recount() {
1206        init_test_behaviors();
1207        let sections = Sections::from_owned(vec![ChunkSection::new_empty()].into_boxed_slice());
1208        let holder = &sections.sections[0];
1209
1210        sections.write_block_batch(&[(1, 2, 3, vanilla_blocks::STONE.default_state())]);
1211        assert!(!entire_section_is_stable_air(&holder.read()));
1212        holder.write().recalculate_counts();
1213        assert!(!holder.read().stable_air_box_is_clear(1, 1, 2, 2, 3, 3));
1214
1215        sections.write_column_blocks(1, 3, &[(2, vanilla_blocks::AIR.default_state())]);
1216        assert!(!entire_section_is_stable_air(&holder.read()));
1217        holder.write().recalculate_counts();
1218        assert!(entire_section_is_stable_air(&holder.read()));
1219    }
1220
1221    #[test]
1222    fn recount_uses_homogeneous_palette_frequency() {
1223        init_test_behaviors();
1224
1225        let mut section = ChunkSection::new_with_biomes(
1226            BlockPalette::Homogeneous(vanilla_blocks::LAVA.default_state()),
1227            plains_biomes(),
1228        );
1229
1230        section.recalculate_counts();
1231
1232        assert_eq!(section.non_empty_block_count(), BLOCKS_PER_SECTION);
1233        assert_eq!(section.fluid_count(), BLOCKS_PER_SECTION);
1234        assert_eq!(section.ticking_block_count(), BLOCKS_PER_SECTION);
1235        assert_eq!(section.ticking_fluid_count(), BLOCKS_PER_SECTION);
1236    }
1237
1238    #[test]
1239    fn recount_uses_heterogeneous_palette_frequencies() {
1240        init_test_behaviors();
1241
1242        let air = vanilla_blocks::AIR.default_state();
1243        let stone = vanilla_blocks::STONE.default_state();
1244        let water = vanilla_blocks::WATER.default_state();
1245        let lava = vanilla_blocks::LAVA.default_state();
1246        let mut cube = Box::new([[[air; 16]; 16]; 16]);
1247
1248        cube[0][0][0] = stone;
1249        cube[1][0][0] = stone;
1250        cube[2][0][0] = water;
1251        cube[3][0][0] = water;
1252        cube[4][0][0] = water;
1253        cube[5][0][0] = lava;
1254
1255        let mut section =
1256            ChunkSection::new_with_biomes(BlockPalette::from_cube(cube), plains_biomes());
1257
1258        section.recalculate_counts();
1259
1260        assert_eq!(section.non_empty_block_count(), 6);
1261        assert_eq!(section.fluid_count(), 4);
1262        assert_eq!(section.ticking_block_count(), 1);
1263        assert_eq!(section.ticking_fluid_count(), 1);
1264    }
1265
1266    #[test]
1267    fn holder_keeps_random_tick_eligibility_in_sync() {
1268        init_test_behaviors();
1269
1270        let mut loaded_section = ChunkSection::new_with_biomes(
1271            BlockPalette::Homogeneous(vanilla_blocks::LAVA.default_state()),
1272            plains_biomes(),
1273        );
1274        loaded_section.recalculate_counts();
1275        let loaded_holder = SectionHolder::new(loaded_section);
1276        assert!(loaded_holder.is_randomly_ticking());
1277
1278        let holder = SectionHolder::new(ChunkSection::new_empty());
1279        {
1280            let mut section = holder.write();
1281            section.set_block_state(0, 0, 0, vanilla_blocks::LAVA.default_state());
1282            assert_eq!(section.ticking_block_count(), 1);
1283            assert_eq!(section.ticking_fluid_count(), 1);
1284        }
1285        assert!(holder.is_randomly_ticking());
1286
1287        {
1288            let Some(mut section) = holder.try_write() else {
1289                panic!("uncontended section write lock was unavailable");
1290            };
1291            section.set_block_state(0, 0, 0, vanilla_blocks::AIR.default_state());
1292            assert_eq!(section.ticking_block_count(), 0);
1293            assert_eq!(section.ticking_fluid_count(), 0);
1294        }
1295        assert!(!holder.is_randomly_ticking());
1296    }
1297
1298    #[test]
1299    fn shared_random_tick_section_bits_follow_cross_word_updates() {
1300        init_test_behaviors();
1301        let sections = Sections::from_owned(
1302            (0..65)
1303                .map(|_| ChunkSection::new_empty())
1304                .collect::<Vec<_>>()
1305                .into_boxed_slice(),
1306        );
1307        let bits = Arc::clone(sections.random_tick_sections());
1308        assert!(bits.is_empty());
1309
1310        {
1311            let mut section = sections.sections[64].write();
1312            section.set_block_state(0, 0, 0, vanilla_blocks::LAVA.default_state());
1313        }
1314        assert_eq!(bits.next(0), Some(64));
1315
1316        {
1317            let mut section = sections.sections[1].write();
1318            section.set_block_state(0, 0, 0, vanilla_blocks::LAVA.default_state());
1319        }
1320        assert_eq!(bits.next(0), Some(1));
1321        assert_eq!(bits.next(2), Some(64));
1322
1323        {
1324            let mut section = sections.sections[1].write();
1325            section.set_block_state(0, 0, 0, vanilla_blocks::AIR.default_state());
1326        }
1327        assert_eq!(bits.next(0), Some(64));
1328
1329        {
1330            let mut section = sections.sections[64].write();
1331            section.set_block_state(0, 0, 0, vanilla_blocks::AIR.default_state());
1332        }
1333        assert!(bits.is_empty());
1334    }
1335
1336    #[test]
1337    fn generation_recount_publishes_random_tick_section_bit() {
1338        init_test_behaviors();
1339        let sections = Sections::from_owned(vec![ChunkSection::new_empty()].into_boxed_slice());
1340        let bits = Arc::clone(sections.random_tick_sections());
1341
1342        {
1343            let mut section = sections.sections[0].write();
1344            section.set_block_state_for_generation(0, 0, 0, vanilla_blocks::LAVA.default_state());
1345        }
1346        assert!(bits.is_empty());
1347
1348        {
1349            let mut section = sections.sections[0].write();
1350            section.finalize_generation_counts_if_needed();
1351        }
1352        assert_eq!(bits.next(0), Some(0));
1353    }
1354
1355    #[test]
1356    fn counter_aware_write_recounts_building_palette_before_delta() {
1357        init_test_behaviors();
1358
1359        let air = vanilla_blocks::AIR.default_state();
1360        let stone = vanilla_blocks::STONE.default_state();
1361        let mut section = ChunkSection::new_empty();
1362
1363        section.set_block_state_for_generation(0, 0, 0, stone);
1364        assert_eq!(section.non_empty_block_count(), 0);
1365
1366        let old_state = section.set_block_state(0, 0, 0, air);
1367
1368        assert_eq!(old_state, stone);
1369        assert_eq!(section.non_empty_block_count(), 0);
1370
1371        let old_state = section.set_block_state(0, 0, 0, stone);
1372
1373        assert_eq!(old_state, air);
1374        assert_eq!(section.non_empty_block_count(), 1);
1375    }
1376}