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 steel_registry::blocks::block_state_ext::BlockStateExt;
14use steel_registry::vanilla_biomes;
15use steel_registry::{REGISTRY, RegistryEntry};
16use steel_utils::{BlockPos, BlockStateId, ChunkPos, locks::SyncRwLock, serial::WriteTo};
17
18use crate::chunk::paletted_container::{BiomePalette, BlockPalette};
19
20/// Lock-free index of sections containing randomly-ticking blocks or fluids.
21///
22/// Section writers update their bit while still holding the section lock. Readers
23/// use relaxed loads because this metadata only decides whether to attempt work;
24/// section contents remain protected by the section lock and brief staleness is
25/// acceptable in the same way as Vanilla's unsynchronized derived counters.
26#[derive(Debug)]
27pub(crate) struct RandomTickSectionBits {
28    words: Box<[AtomicU64]>,
29    section_count: usize,
30}
31
32impl RandomTickSectionBits {
33    fn new(section_count: usize) -> Self {
34        let word_count = section_count.div_ceil(u64::BITS as usize);
35        let words = (0..word_count)
36            .map(|_| AtomicU64::new(0))
37            .collect::<Vec<_>>()
38            .into_boxed_slice();
39        Self {
40            words,
41            section_count,
42        }
43    }
44
45    fn set(&self, section_index: usize, randomly_ticking: bool) {
46        debug_assert!(section_index < self.section_count);
47        let word_index = section_index / u64::BITS as usize;
48        let mask = 1_u64 << (section_index % u64::BITS as usize);
49        if randomly_ticking {
50            self.words[word_index].fetch_or(mask, Ordering::Relaxed);
51        } else {
52            self.words[word_index].fetch_and(!mask, Ordering::Relaxed);
53        }
54    }
55
56    fn contains(&self, section_index: usize) -> bool {
57        debug_assert!(section_index < self.section_count);
58        let word_index = section_index / u64::BITS as usize;
59        let mask = 1_u64 << (section_index % u64::BITS as usize);
60        self.words[word_index].load(Ordering::Relaxed) & mask != 0
61    }
62
63    /// Returns the next eligible section at or above `start`.
64    ///
65    /// Each call reloads the current word, so a random-tick callback changing a
66    /// later section can affect that later section in the same chunk pass.
67    #[must_use]
68    pub(crate) fn next(&self, start: usize) -> Option<usize> {
69        if start >= self.section_count {
70            return None;
71        }
72
73        let mut word_index = start / u64::BITS as usize;
74        let bit_index = start % u64::BITS as usize;
75        let mut bits = self.words[word_index].load(Ordering::Relaxed) & (u64::MAX << bit_index);
76        loop {
77            if bits != 0 {
78                let section_index =
79                    word_index * u64::BITS as usize + bits.trailing_zeros() as usize;
80                return (section_index < self.section_count).then_some(section_index);
81            }
82            word_index += 1;
83            let word = self.words.get(word_index)?;
84            bits = word.load(Ordering::Relaxed);
85        }
86    }
87
88    #[inline]
89    #[must_use]
90    pub(crate) fn is_empty(&self) -> bool {
91        self.next(0).is_none()
92    }
93}
94
95/// A wrapper around a chunk section.
96#[derive(Debug)]
97pub struct SectionHolder {
98    /// The chunk section data (requires lock to access).
99    section: SyncRwLock<ChunkSection>,
100    /// Shared lock-free random-tick section index.
101    randomly_ticking_sections: Arc<RandomTickSectionBits>,
102    section_index: usize,
103}
104
105impl SectionHolder {
106    /// Creates a new section holder.
107    #[must_use]
108    pub fn new(section: ChunkSection) -> Self {
109        let randomly_ticking_sections = Arc::new(RandomTickSectionBits::new(1));
110        Self::with_random_tick_index(section, randomly_ticking_sections, 0)
111    }
112
113    fn with_random_tick_index(
114        section: ChunkSection,
115        randomly_ticking_sections: Arc<RandomTickSectionBits>,
116        section_index: usize,
117    ) -> Self {
118        let randomly_ticking = section.is_randomly_ticking();
119        let result = Self {
120            section: SyncRwLock::new(section),
121            randomly_ticking_sections,
122            section_index,
123        };
124        if randomly_ticking {
125            result.randomly_ticking_sections.set(section_index, true);
126        }
127        result
128    }
129
130    /// Returns true if this section contains any randomly-ticking blocks or fluids.
131    ///
132    /// The mirror may briefly be stale relative to a concurrent section writer.
133    /// Section contents and the authoritative counter remain protected by the
134    /// section lock.
135    #[inline]
136    #[must_use]
137    pub fn is_randomly_ticking(&self) -> bool {
138        self.randomly_ticking_sections.contains(self.section_index)
139    }
140
141    /// Acquires a read lock on the section.
142    #[inline]
143    pub fn read(&self) -> RwLockReadGuard<'_, ChunkSection> {
144        self.section.read()
145    }
146
147    /// Attempts to acquire a read lock on the section.
148    #[inline]
149    pub fn try_read(&self) -> Option<RwLockReadGuard<'_, ChunkSection>> {
150        self.section.try_read()
151    }
152
153    /// Acquires a write lock on the section.
154    #[inline]
155    pub fn write(&self) -> SectionWriteGuard<'_> {
156        SectionWriteGuard::new(
157            self.section.write(),
158            &self.randomly_ticking_sections,
159            self.section_index,
160        )
161    }
162
163    /// Attempts to acquire a write lock on the section.
164    #[inline]
165    pub fn try_write(&self) -> Option<SectionWriteGuard<'_>> {
166        self.section.try_write().map(|guard| {
167            SectionWriteGuard::new(guard, &self.randomly_ticking_sections, self.section_index)
168        })
169    }
170}
171
172/// A chunk-section write guard that republishes derived lock-free metadata.
173pub struct SectionWriteGuard<'a> {
174    guard: RwLockWriteGuard<'a, ChunkSection>,
175    randomly_ticking_sections: &'a RandomTickSectionBits,
176    section_index: usize,
177    was_randomly_ticking: bool,
178}
179
180impl<'a> SectionWriteGuard<'a> {
181    fn new(
182        guard: RwLockWriteGuard<'a, ChunkSection>,
183        randomly_ticking_sections: &'a RandomTickSectionBits,
184        section_index: usize,
185    ) -> Self {
186        let was_randomly_ticking = guard.is_randomly_ticking();
187        Self {
188            guard,
189            randomly_ticking_sections,
190            section_index,
191            was_randomly_ticking,
192        }
193    }
194}
195
196impl Deref for SectionWriteGuard<'_> {
197    type Target = ChunkSection;
198
199    fn deref(&self) -> &Self::Target {
200        &self.guard
201    }
202}
203
204impl DerefMut for SectionWriteGuard<'_> {
205    fn deref_mut(&mut self) -> &mut Self::Target {
206        &mut self.guard
207    }
208}
209
210impl Drop for SectionWriteGuard<'_> {
211    fn drop(&mut self) {
212        let is_randomly_ticking = self.guard.is_randomly_ticking();
213        if is_randomly_ticking != self.was_randomly_ticking {
214            self.randomly_ticking_sections
215                .set(self.section_index, is_randomly_ticking);
216        }
217    }
218}
219
220/// A collection of chunk sections.
221#[derive(Debug)]
222pub struct Sections {
223    /// The sections in the collection.
224    pub sections: Box<[SectionHolder]>,
225    randomly_ticking_sections: Arc<RandomTickSectionBits>,
226}
227
228/// Cached section counter traits for one block state.
229#[derive(Clone, Copy, Debug, PartialEq, Eq)]
230pub(crate) struct BlockStateSectionCounts {
231    is_air: bool,
232    has_fluid: bool,
233    randomly_ticking_block: bool,
234    randomly_ticking_fluid: bool,
235}
236
237const BLOCKS_PER_SECTION: u16 = 16 * 16 * 16;
238
239impl Sections {
240    /// Creates a new `Sections` from a box of owned `ChunkSection`s.
241    #[must_use]
242    pub fn from_owned(sections: Box<[ChunkSection]>) -> Self {
243        let randomly_ticking_sections = Arc::new(RandomTickSectionBits::new(sections.len()));
244        let holders: Box<[SectionHolder]> = sections
245            .into_vec()
246            .into_iter()
247            .enumerate()
248            .map(|(section_index, section)| {
249                SectionHolder::with_random_tick_index(
250                    section,
251                    Arc::clone(&randomly_ticking_sections),
252                    section_index,
253                )
254            })
255            .collect();
256        Self {
257            sections: holders,
258            randomly_ticking_sections,
259        }
260    }
261
262    /// Returns the shared lock-free random-tick section index.
263    #[must_use]
264    pub(crate) const fn random_tick_sections(&self) -> &Arc<RandomTickSectionBits> {
265        &self.randomly_ticking_sections
266    }
267
268    /// Gets a block at a relative position in the chunk.
269    #[must_use]
270    pub fn get_relative_block(
271        &self,
272        relative_x: usize,
273        relative_y: usize,
274        relative_z: usize,
275    ) -> Option<BlockStateId> {
276        debug_assert!(relative_x < BlockPalette::SIZE);
277        debug_assert!(relative_z < BlockPalette::SIZE);
278
279        let section_index = relative_y / BlockPalette::SIZE;
280        let relative_y = relative_y % BlockPalette::SIZE;
281        self.sections.get(section_index).map(|section| {
282            section
283                .read()
284                .states
285                .get(relative_x, relative_y, relative_z)
286        })
287    }
288
289    /// Reads an entire column at `(x, z)` across all sections into a caller-owned buffer.
290    ///
291    /// Holds each section's read lock once for 16 Y reads instead of acquiring
292    /// a lock per block. Indexed by `relative_y` (0 = chunk min-y).
293    /// The buffer is resized if needed and reused across calls to avoid allocation.
294    pub fn read_column_into(&self, x: usize, z: usize, buf: &mut Vec<BlockStateId>) {
295        debug_assert!(x < BlockPalette::SIZE);
296        debug_assert!(z < BlockPalette::SIZE);
297
298        let total = self.sections.len() * 16;
299        if buf.len() != total {
300            buf.resize(total, BlockStateId::default());
301        }
302        for (i, holder) in self.sections.iter().enumerate() {
303            let guard = holder.read();
304            let base = i * 16;
305            guard
306                .states
307                .copy_column_into(x, z, &mut buf[base..base + 16]);
308        }
309    }
310
311    /// Reads all biome palette values into a flat array.
312    ///
313    /// Indexed as `[section_idx * 64 + qy * 16 + qz * 4 + qx]`.
314    /// Holds each section's read lock once for all 64 biome reads.
315    #[must_use]
316    pub fn read_all_biomes(&self) -> Box<[u16]> {
317        let total = self.sections.len() * 64;
318        let mut biomes = vec![0u16; total];
319        for (i, holder) in self.sections.iter().enumerate() {
320            let guard = holder.read();
321            let base = i * 64;
322            for qy in 0..4 {
323                for qz in 0..4 {
324                    for qx in 0..4 {
325                        biomes[base + qy * 16 + qz * 4 + qx] = guard.biomes.get(qx, qy, qz);
326                    }
327                }
328            }
329        }
330        biomes.into_boxed_slice()
331    }
332
333    /// Visits every biome palette value in section order while holding each
334    /// section's read lock once.
335    pub fn for_each_biome_id(&self, mut visitor: impl FnMut(u16)) {
336        for holder in &self.sections {
337            let guard = holder.read();
338            for qy in 0..4 {
339                for qz in 0..4 {
340                    for qx in 0..4 {
341                        visitor(guard.biomes.get(qx, qy, qz));
342                    }
343                }
344            }
345        }
346    }
347
348    /// Returns whether each real chunk section contains no non-air blocks.
349    #[must_use]
350    pub fn section_emptiness_map(&self) -> Box<[bool]> {
351        self.sections
352            .iter()
353            .map(|section| section.read().is_empty())
354            .collect()
355    }
356
357    /// Returns block-light source positions in `ScalableLux` section/local-index order.
358    #[must_use]
359    pub fn block_light_sources(&self, chunk_pos: ChunkPos, min_y: i32) -> Vec<BlockPos> {
360        let mut sources = Vec::new();
361        let chunk_min_x = chunk_pos.0.x * BlockPalette::SIZE as i32;
362        let chunk_min_z = chunk_pos.0.y * BlockPalette::SIZE as i32;
363
364        for (section_index, section) in self.sections.iter().enumerate() {
365            let section_min_y = min_y + (section_index * BlockPalette::SIZE) as i32;
366            section.read().append_block_light_sources(
367                chunk_min_x,
368                section_min_y,
369                chunk_min_z,
370                &mut sources,
371            );
372        }
373
374        sources
375    }
376
377    /// Writes multiple blocks in one column, holding each section's write guard
378    /// across all writes to that section. Most efficient when blocks are grouped
379    /// by section (e.g. descending `relative_y` from a top-to-bottom scan).
380    pub fn write_column_blocks(&self, x: usize, z: usize, blocks: &[(usize, BlockStateId)]) {
381        const DIM: usize = BlockPalette::SIZE;
382        debug_assert!(x < DIM);
383        debug_assert!(z < DIM);
384
385        let mut i = 0;
386        while i < blocks.len() {
387            let section_idx = blocks[i].0 / DIM;
388            let mut guard = self.sections[section_idx].write();
389            guard.states.enter_building_mode();
390            let Some(cube) = guard.states.as_building_slice_mut() else {
391                unreachable!("just entered building mode")
392            };
393            let xz_base = z * DIM + x;
394            while i < blocks.len() && blocks[i].0 / DIM == section_idx {
395                let (rel_y, value) = blocks[i];
396                let local_y = rel_y % DIM;
397                cube[local_y * DIM * DIM + xz_base] = value;
398                i += 1;
399            }
400        }
401    }
402
403    /// Writes a batch of blocks at arbitrary positions, holding each section's
404    /// write guard across consecutive entries in the same section. Blocks should
405    /// be roughly grouped by section index for best performance.
406    ///
407    /// Each touched section enters worldgen Building mode (raw cube, no palette
408    /// tracking) so writes are O(1) stores. Per-write goes through a flat
409    /// `&mut [V]` view of the cube — bypasses the 3-arm `set` match and the
410    /// unused old-value load. `recalculate_counts` finalizes.
411    pub fn write_block_batch(&self, blocks: &[(usize, usize, usize, BlockStateId)]) {
412        const DIM: usize = BlockPalette::SIZE;
413        let mut i = 0;
414        while i < blocks.len() {
415            let section_idx = blocks[i].1 / DIM;
416            let mut guard = self.sections[section_idx].write();
417            guard.states.enter_building_mode();
418            let Some(cube) = guard.states.as_building_slice_mut() else {
419                // enter_building_mode just transitioned to Building.
420                unreachable!("just entered building mode")
421            };
422            while i < blocks.len() && blocks[i].1 / DIM == section_idx {
423                let (x, rel_y, z, value) = blocks[i];
424                let local_y = rel_y % DIM;
425                cube[local_y * DIM * DIM + z * DIM + x] = value;
426                i += 1;
427            }
428        }
429    }
430
431    /// Writes a batch of blocks while maintaining section counters and palette state.
432    ///
433    /// Blocks should be grouped by section index so each touched section only needs
434    /// one write guard.
435    pub(crate) fn write_tracked_block_batch(&self, blocks: &[(usize, usize, usize, BlockStateId)]) {
436        const DIM: usize = BlockPalette::SIZE;
437        let mut i = 0;
438        while i < blocks.len() {
439            let section_idx = blocks[i].1 / DIM;
440            let mut guard = self.sections[section_idx].write();
441            while i < blocks.len() && blocks[i].1 / DIM == section_idx {
442                let (x, relative_y, z, value) = blocks[i];
443                guard.set_block_state(x, relative_y % DIM, z, value);
444                i += 1;
445            }
446        }
447    }
448
449    /// Sets a block at a relative position in the chunk and keeps section
450    /// counters/palette serialization ready.
451    pub fn set_relative_block(
452        &self,
453        relative_x: usize,
454        relative_y: usize,
455        relative_z: usize,
456        value: BlockStateId,
457    ) {
458        debug_assert!(relative_x < BlockPalette::SIZE);
459        debug_assert!(relative_z < BlockPalette::SIZE);
460
461        let idx = relative_y / BlockPalette::SIZE;
462        let relative_y = relative_y % BlockPalette::SIZE;
463        let mut guard = self.sections[idx].write();
464        guard.set_block_state(relative_x, relative_y, relative_z, value);
465    }
466
467    /// Sets a block during worldgen using the raw building palette path.
468    ///
469    /// Callers must finalize by recounting touched sections before save,
470    /// promotion, or packet serialization.
471    pub(crate) fn set_relative_block_for_generation(
472        &self,
473        relative_x: usize,
474        relative_y: usize,
475        relative_z: usize,
476        value: BlockStateId,
477    ) {
478        debug_assert!(relative_x < BlockPalette::SIZE);
479        debug_assert!(relative_z < BlockPalette::SIZE);
480
481        let idx = relative_y / BlockPalette::SIZE;
482        let relative_y = relative_y % BlockPalette::SIZE;
483        let mut guard = self.sections[idx].write();
484        guard.set_block_state_for_generation(relative_x, relative_y, relative_z, value);
485    }
486}
487
488/// A chunk section.
489///
490/// Contains a 16x16x16 cube of block states and biomes, along with cached
491/// counts for optimization (similar to vanilla's `LevelChunkSection`).
492#[derive(Debug)]
493pub struct ChunkSection {
494    /// The block states in the section.
495    pub states: BlockPalette,
496    /// The biomes in the section.
497    pub biomes: BiomePalette,
498    /// Number of non-air blocks in this section (0-4096).
499    /// Used to quickly check if a section is empty.
500    non_empty_block_count: u16,
501    /// Number of fluid-containing blocks in this section (0-4096).
502    /// Includes water, lava, and waterlogged blocks.
503    fluid_count: u16,
504    /// Number of randomly-ticking blocks in this section (0-4096).
505    pub ticking_block_count: u16,
506    /// Number of randomly-ticking fluids in this section (0-4096).
507    ticking_fluid_count: u16,
508}
509
510impl ChunkSection {
511    /// Creates a new chunk section with the given block states and biomes.
512    ///
513    /// Note: You must call `recalculate_counts()` after creation to initialize
514    /// the cached counters if the states palette contains non-air blocks.
515    #[must_use]
516    pub const fn new_with_biomes(states: BlockPalette, biomes: BiomePalette) -> Self {
517        Self {
518            states,
519            biomes,
520            non_empty_block_count: 0,
521            fluid_count: 0,
522            ticking_block_count: 0,
523            ticking_fluid_count: 0,
524        }
525    }
526
527    /// Creates a new empty chunk section.
528    #[must_use]
529    pub fn new_empty() -> Self {
530        let plains_id = vanilla_biomes::PLAINS.id() as u16;
531        Self {
532            states: BlockPalette::Homogeneous(BlockStateId(0)),
533            biomes: BiomePalette::Homogeneous(plains_id),
534            non_empty_block_count: 0,
535            fluid_count: 0,
536            ticking_block_count: 0,
537            ticking_fluid_count: 0,
538        }
539    }
540
541    /// Returns true if this section contains no non-air blocks.
542    #[must_use]
543    pub const fn is_empty(&self) -> bool {
544        self.non_empty_block_count == 0
545    }
546
547    /// Returns true if this section contains any randomly-ticking blocks or fluids.
548    #[must_use]
549    pub const fn is_randomly_ticking(&self) -> bool {
550        self.is_randomly_ticking_blocks() || self.is_randomly_ticking_fluids()
551    }
552
553    /// Returns true if this section contains any randomly-ticking blocks.
554    #[must_use]
555    pub const fn is_randomly_ticking_blocks(&self) -> bool {
556        self.ticking_block_count > 0
557    }
558
559    /// Returns true if this section contains any randomly-ticking fluids.
560    #[must_use]
561    pub const fn is_randomly_ticking_fluids(&self) -> bool {
562        self.ticking_fluid_count > 0
563    }
564
565    /// Returns true if this section's palette may contain block-light sources.
566    #[must_use]
567    pub fn maybe_has_block_light_sources(&self) -> bool {
568        !self.is_empty()
569            && self
570                .states
571                .maybe_has(|state| state.get_light_emission() > 0)
572    }
573
574    /// Appends block-light source positions in `ScalableLux` local-index order.
575    pub fn append_block_light_sources(
576        &self,
577        chunk_min_x: i32,
578        section_min_y: i32,
579        chunk_min_z: i32,
580        sources: &mut Vec<BlockPos>,
581    ) {
582        if !self.maybe_has_block_light_sources() {
583            return;
584        }
585
586        for local_index in 0..BlockPalette::VOLUME {
587            let state = self.states.get_at_index(local_index);
588            if state.get_light_emission() == 0 {
589                continue;
590            }
591
592            sources.push(BlockPos::new(
593                chunk_min_x + (local_index & 15) as i32,
594                section_min_y + (local_index >> 8) as i32,
595                chunk_min_z + ((local_index >> 4) & 15) as i32,
596            ));
597        }
598    }
599
600    /// Returns the number of non-air blocks in this section.
601    #[must_use]
602    pub const fn non_empty_block_count(&self) -> u16 {
603        self.non_empty_block_count
604    }
605
606    /// Returns the number of fluid-containing blocks in this section.
607    #[must_use]
608    pub const fn fluid_count(&self) -> u16 {
609        self.fluid_count
610    }
611
612    /// Returns if the chunk has fluid.
613    #[must_use]
614    pub const fn has_fluid(&self) -> bool {
615        self.fluid_count > 0
616    }
617
618    /// Returns the number of randomly-ticking blocks in this section.
619    #[must_use]
620    pub const fn ticking_block_count(&self) -> u16 {
621        self.ticking_block_count
622    }
623
624    /// Returns the number of randomly-ticking fluids in this section.
625    #[must_use]
626    pub const fn ticking_fluid_count(&self) -> u16 {
627        self.ticking_fluid_count
628    }
629
630    /// Recalculates cached counters from extracted per-state metadata.
631    ///
632    /// Iterates the palette (`O(palette_size)`) rather than every cube cell
633    /// (`O(4096)`): each block-state appears at most once in the palette and
634    /// carries its own occurrence count, so we just classify each unique state
635    /// and multiply by its count. Mirrors Moonrise's `BlockCountingBitStorage`.
636    /// For a `Homogeneous` section that's a single classify; for typical
637    /// `Heterogeneous` sections palette is well under 16 entries.
638    pub fn recalculate_counts(&mut self) {
639        self.recalculate_counts_from_palette(Self::block_state_section_counts);
640    }
641
642    fn recalculate_counts_from_palette(
643        &mut self,
644        mut counts_for_state: impl FnMut(BlockStateId) -> BlockStateSectionCounts,
645    ) {
646        self.states.finalize_building();
647
648        let mut non_empty: u16 = 0;
649        let mut fluid: u16 = 0;
650        let mut ticking_blocks: u16 = 0;
651        let mut ticking_fluids: u16 = 0;
652
653        match &self.states {
654            BlockPalette::Homogeneous(state) => {
655                let counts = counts_for_state(*state);
656                Self::accumulate_counter_traits(
657                    &mut non_empty,
658                    &mut fluid,
659                    &mut ticking_blocks,
660                    &mut ticking_fluids,
661                    counts,
662                    BLOCKS_PER_SECTION,
663                );
664            }
665            BlockPalette::Heterogeneous(data) => {
666                for &(state, count) in &data.palette {
667                    let counts = counts_for_state(state);
668                    Self::accumulate_counter_traits(
669                        &mut non_empty,
670                        &mut fluid,
671                        &mut ticking_blocks,
672                        &mut ticking_fluids,
673                        counts,
674                        count,
675                    );
676                }
677            }
678            BlockPalette::Building(_) => unreachable!("finalize_building was just called"),
679        }
680
681        self.non_empty_block_count = non_empty;
682        self.fluid_count = fluid;
683        self.ticking_block_count = ticking_blocks;
684        self.ticking_fluid_count = ticking_fluids;
685    }
686
687    const fn accumulate_counter_traits(
688        non_empty: &mut u16,
689        fluid: &mut u16,
690        ticking_blocks: &mut u16,
691        ticking_fluids: &mut u16,
692        counts: BlockStateSectionCounts,
693        block_count: u16,
694    ) {
695        if !counts.is_air {
696            *non_empty += block_count;
697        }
698        if counts.has_fluid {
699            *fluid += block_count;
700        }
701        if counts.randomly_ticking_block {
702            *ticking_blocks += block_count;
703        }
704        if counts.randomly_ticking_fluid {
705            *ticking_fluids += block_count;
706        }
707    }
708
709    /// Whether this section's palette contains any POI-type block state.
710    ///
711    /// Lets the Full-stage POI populate skip the full 4096-block
712    /// `scan_and_populate` for the overwhelming majority of sections
713    /// (stone/dirt/air) that hold no POI blocks — a palette scan of `O(≤16)`
714    /// instead of `O(4096)`. Mirrors vanilla's `LevelChunkSection.maybeHas`.
715    #[must_use]
716    pub fn contains_poi(&self) -> bool {
717        let poi = &REGISTRY.poi_types;
718        match &self.states {
719            BlockPalette::Homogeneous(state) => poi.is_poi_state(*state),
720            BlockPalette::Heterogeneous(data) => data
721                .palette
722                .iter()
723                .any(|(state, _)| poi.is_poi_state(*state)),
724            // Not yet finalized (only happens mid-worldgen, not at promotion);
725            // fall back to scanning rather than risk missing a POI.
726            BlockPalette::Building(_) => true,
727        }
728    }
729
730    /// Sets a block state and updates the cached counters.
731    ///
732    /// Returns the old block state.
733    ///
734    pub fn set_block_state(
735        &mut self,
736        x: usize,
737        y: usize,
738        z: usize,
739        new_state: BlockStateId,
740    ) -> BlockStateId {
741        self.ensure_counter_ready_for_delta();
742        let old_state = self.states.set(x, y, z, new_state);
743
744        if old_state != new_state {
745            let old_counts = Self::block_state_section_counts(old_state);
746            let new_counts = Self::block_state_section_counts(new_state);
747            self.apply_count_change(old_counts, new_counts);
748        }
749
750        old_state
751    }
752
753    /// Sets a block state through the raw worldgen building path.
754    ///
755    /// Returns the old block state. Cached counters are intentionally not
756    /// updated; callers must recount before light, promotion, save, or packet
757    /// serialization.
758    pub(crate) fn set_block_state_for_generation(
759        &mut self,
760        x: usize,
761        y: usize,
762        z: usize,
763        new_state: BlockStateId,
764    ) -> BlockStateId {
765        self.states.enter_building_mode();
766        self.states.set(x, y, z, new_state)
767    }
768
769    /// Returns the cached-counter traits for a block state.
770    pub(crate) fn block_state_section_counts(state: BlockStateId) -> BlockStateSectionCounts {
771        let metadata = state.get_ticking_metadata();
772        BlockStateSectionCounts {
773            is_air: metadata.is_air(),
774            has_fluid: metadata.has_fluid(),
775            randomly_ticking_block: metadata.randomly_ticking_block(),
776            randomly_ticking_fluid: metadata.randomly_ticking_fluid(),
777        }
778    }
779
780    pub(crate) fn finalize_generation_counts_if_needed(&mut self) {
781        if matches!(&self.states, BlockPalette::Building(_)) {
782            self.recalculate_counts();
783        }
784    }
785
786    fn ensure_counter_ready_for_delta(&mut self) {
787        if matches!(&self.states, BlockPalette::Building(_)) {
788            log::debug!(
789                "finalizing worldgen Building palette before applying a counter-aware \
790                 block-state delta"
791            );
792            self.recalculate_counts();
793        }
794    }
795
796    const fn apply_count_change(
797        &mut self,
798        old_counts: BlockStateSectionCounts,
799        new_counts: BlockStateSectionCounts,
800    ) {
801        if !old_counts.is_air && new_counts.is_air {
802            self.non_empty_block_count -= 1;
803        } else if old_counts.is_air && !new_counts.is_air {
804            self.non_empty_block_count += 1;
805        }
806
807        if old_counts.has_fluid && !new_counts.has_fluid {
808            self.fluid_count -= 1;
809        } else if !old_counts.has_fluid && new_counts.has_fluid {
810            self.fluid_count += 1;
811        }
812
813        if old_counts.randomly_ticking_block && !new_counts.randomly_ticking_block {
814            self.ticking_block_count -= 1;
815        } else if !old_counts.randomly_ticking_block && new_counts.randomly_ticking_block {
816            self.ticking_block_count += 1;
817        }
818
819        if old_counts.randomly_ticking_fluid && !new_counts.randomly_ticking_fluid {
820            self.ticking_fluid_count -= 1;
821        } else if !old_counts.randomly_ticking_fluid && new_counts.randomly_ticking_fluid {
822            self.ticking_fluid_count += 1;
823        }
824    }
825
826    /// Writes the chunk section to a writer.
827    ///
828    /// # Panics
829    /// - If the writer fails to write.
830    pub fn write(&self, writer: &mut Cursor<Vec<u8>>) {
831        self.non_empty_block_count
832            .write(writer)
833            .expect("Failed to write block count");
834        self.fluid_count
835            .write(writer)
836            .expect("Failed to write fluid count");
837
838        self.states
839            .write(writer)
840            .expect("Failed to write block states");
841        self.biomes.write(writer).expect("Failed to write biomes");
842    }
843}
844
845#[cfg(test)]
846mod tests {
847    use steel_registry::init_vanilla_registry;
848    use steel_registry::vanilla_blocks;
849
850    use crate::behavior::init_behaviors;
851
852    use super::*;
853
854    fn plains_biomes() -> BiomePalette {
855        BiomePalette::Homogeneous(vanilla_biomes::PLAINS.id() as u16)
856    }
857
858    fn init_test_behaviors() {
859        init_vanilla_registry();
860        init_behaviors();
861    }
862
863    #[test]
864    fn recount_uses_homogeneous_palette_frequency() {
865        init_test_behaviors();
866
867        let mut section = ChunkSection::new_with_biomes(
868            BlockPalette::Homogeneous(vanilla_blocks::LAVA.default_state()),
869            plains_biomes(),
870        );
871
872        section.recalculate_counts();
873
874        assert_eq!(section.non_empty_block_count(), BLOCKS_PER_SECTION);
875        assert_eq!(section.fluid_count(), BLOCKS_PER_SECTION);
876        assert_eq!(section.ticking_block_count(), BLOCKS_PER_SECTION);
877        assert_eq!(section.ticking_fluid_count(), BLOCKS_PER_SECTION);
878    }
879
880    #[test]
881    fn recount_uses_heterogeneous_palette_frequencies() {
882        init_test_behaviors();
883
884        let air = vanilla_blocks::AIR.default_state();
885        let stone = vanilla_blocks::STONE.default_state();
886        let water = vanilla_blocks::WATER.default_state();
887        let lava = vanilla_blocks::LAVA.default_state();
888        let mut cube = Box::new([[[air; 16]; 16]; 16]);
889
890        cube[0][0][0] = stone;
891        cube[1][0][0] = stone;
892        cube[2][0][0] = water;
893        cube[3][0][0] = water;
894        cube[4][0][0] = water;
895        cube[5][0][0] = lava;
896
897        let mut section =
898            ChunkSection::new_with_biomes(BlockPalette::from_cube(cube), plains_biomes());
899
900        section.recalculate_counts();
901
902        assert_eq!(section.non_empty_block_count(), 6);
903        assert_eq!(section.fluid_count(), 4);
904        assert_eq!(section.ticking_block_count(), 1);
905        assert_eq!(section.ticking_fluid_count(), 1);
906    }
907
908    #[test]
909    fn holder_keeps_random_tick_eligibility_in_sync() {
910        init_test_behaviors();
911
912        let mut loaded_section = ChunkSection::new_with_biomes(
913            BlockPalette::Homogeneous(vanilla_blocks::LAVA.default_state()),
914            plains_biomes(),
915        );
916        loaded_section.recalculate_counts();
917        let loaded_holder = SectionHolder::new(loaded_section);
918        assert!(loaded_holder.is_randomly_ticking());
919
920        let holder = SectionHolder::new(ChunkSection::new_empty());
921        {
922            let mut section = holder.write();
923            section.set_block_state(0, 0, 0, vanilla_blocks::LAVA.default_state());
924            assert_eq!(section.ticking_block_count(), 1);
925            assert_eq!(section.ticking_fluid_count(), 1);
926        }
927        assert!(holder.is_randomly_ticking());
928
929        {
930            let Some(mut section) = holder.try_write() else {
931                panic!("uncontended section write lock was unavailable");
932            };
933            section.set_block_state(0, 0, 0, vanilla_blocks::AIR.default_state());
934            assert_eq!(section.ticking_block_count(), 0);
935            assert_eq!(section.ticking_fluid_count(), 0);
936        }
937        assert!(!holder.is_randomly_ticking());
938    }
939
940    #[test]
941    fn shared_random_tick_section_bits_follow_cross_word_updates() {
942        init_test_behaviors();
943        let sections = Sections::from_owned(
944            (0..65)
945                .map(|_| ChunkSection::new_empty())
946                .collect::<Vec<_>>()
947                .into_boxed_slice(),
948        );
949        let bits = Arc::clone(sections.random_tick_sections());
950        assert!(bits.is_empty());
951
952        {
953            let mut section = sections.sections[64].write();
954            section.set_block_state(0, 0, 0, vanilla_blocks::LAVA.default_state());
955        }
956        assert_eq!(bits.next(0), Some(64));
957
958        {
959            let mut section = sections.sections[1].write();
960            section.set_block_state(0, 0, 0, vanilla_blocks::LAVA.default_state());
961        }
962        assert_eq!(bits.next(0), Some(1));
963        assert_eq!(bits.next(2), Some(64));
964
965        {
966            let mut section = sections.sections[1].write();
967            section.set_block_state(0, 0, 0, vanilla_blocks::AIR.default_state());
968        }
969        assert_eq!(bits.next(0), Some(64));
970
971        {
972            let mut section = sections.sections[64].write();
973            section.set_block_state(0, 0, 0, vanilla_blocks::AIR.default_state());
974        }
975        assert!(bits.is_empty());
976    }
977
978    #[test]
979    fn generation_recount_publishes_random_tick_section_bit() {
980        init_test_behaviors();
981        let sections = Sections::from_owned(vec![ChunkSection::new_empty()].into_boxed_slice());
982        let bits = Arc::clone(sections.random_tick_sections());
983
984        {
985            let mut section = sections.sections[0].write();
986            section.set_block_state_for_generation(0, 0, 0, vanilla_blocks::LAVA.default_state());
987        }
988        assert!(bits.is_empty());
989
990        {
991            let mut section = sections.sections[0].write();
992            section.finalize_generation_counts_if_needed();
993        }
994        assert_eq!(bits.next(0), Some(0));
995    }
996
997    #[test]
998    fn counter_aware_write_recounts_building_palette_before_delta() {
999        init_test_behaviors();
1000
1001        let air = vanilla_blocks::AIR.default_state();
1002        let stone = vanilla_blocks::STONE.default_state();
1003        let mut section = ChunkSection::new_empty();
1004
1005        section.set_block_state_for_generation(0, 0, 0, stone);
1006        assert_eq!(section.non_empty_block_count(), 0);
1007
1008        let old_state = section.set_block_state(0, 0, 0, air);
1009
1010        assert_eq!(old_state, stone);
1011        assert_eq!(section.non_empty_block_count(), 0);
1012
1013        let old_state = section.set_block_state(0, 0, 0, stone);
1014
1015        assert_eq!(old_state, air);
1016        assert_eq!(section.non_empty_block_count(), 1);
1017    }
1018}