Skip to main content

steel_core/chunk/light/
storage.rs

1use steel_utils::{BlockPos, SectionPos};
2
3use crate::chunk::section::Sections;
4
5use super::{
6    DATA_LAYER_EDGE, DATA_LAYER_SIZE, LightLayer, LightSectionRange, LightSectionRangeError,
7    MAX_LIGHT_LEVEL,
8};
9
10/// Error returned when a chunk light emptiness map has the wrong length.
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub struct ChunkLightEmptinessMapLengthError {
13    /// Expected section count.
14    pub expected: usize,
15    /// Actual section count.
16    pub actual: usize,
17}
18
19/// Storage representation for one present light section.
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub enum LightSectionData {
22    /// One light value applies to the whole section.
23    Homogeneous(u8),
24    /// Vanilla low-nibble-first packed light values.
25    Packed(Box<[u8; DATA_LAYER_SIZE]>),
26}
27
28impl LightSectionData {
29    /// Creates homogeneous section data, masking to the vanilla light range.
30    #[must_use]
31    pub const fn homogeneous(value: u8) -> Self {
32        Self::Homogeneous(value & MAX_LIGHT_LEVEL)
33    }
34
35    /// Returns the light value at local section coordinates.
36    #[must_use]
37    pub fn get(&self, x: usize, y: usize, z: usize) -> u8 {
38        debug_assert!(x < DATA_LAYER_EDGE);
39        debug_assert!(y < DATA_LAYER_EDGE);
40        debug_assert!(z < DATA_LAYER_EDGE);
41
42        match self {
43            Self::Homogeneous(value) => *value,
44            Self::Packed(data) => Self::get_from_packed(data, Self::index(x, y, z)),
45        }
46    }
47
48    /// Sets the light value at local section coordinates.
49    pub fn set(&mut self, x: usize, y: usize, z: usize, value: u8) {
50        debug_assert!(x < DATA_LAYER_EDGE);
51        debug_assert!(y < DATA_LAYER_EDGE);
52        debug_assert!(z < DATA_LAYER_EDGE);
53
54        let index = Self::index(x, y, z);
55        match self {
56            Self::Homogeneous(default_value) => {
57                let mut data = Box::new([Self::pack_filled(*default_value); DATA_LAYER_SIZE]);
58                Self::set_in_packed(&mut data, index, value);
59                *self = Self::Packed(data);
60            }
61            Self::Packed(data) => Self::set_in_packed(data, index, value),
62        }
63    }
64
65    /// Fills the whole section with one value.
66    pub fn fill(&mut self, value: u8) {
67        *self = Self::homogeneous(value);
68    }
69
70    /// Returns true when this section is represented by one homogeneous value.
71    #[must_use]
72    pub const fn is_homogeneous(&self) -> bool {
73        matches!(self, Self::Homogeneous(_))
74    }
75
76    /// Returns true when this section is the visible empty section state.
77    #[must_use]
78    pub const fn is_empty(&self) -> bool {
79        matches!(self, Self::Homogeneous(0))
80    }
81
82    /// Returns true when every packed light value is zero.
83    #[must_use]
84    pub fn is_all_zero(&self) -> bool {
85        match self {
86            Self::Homogeneous(value) => *value == 0,
87            Self::Packed(data) => data.iter().all(|value| *value == 0),
88        }
89    }
90
91    /// Returns packed bytes without changing the section representation.
92    #[must_use]
93    pub fn to_bytes(&self) -> Box<[u8; DATA_LAYER_SIZE]> {
94        match self {
95            Self::Homogeneous(value) => Box::new([Self::pack_filled(*value); DATA_LAYER_SIZE]),
96            Self::Packed(data) => Box::new(**data),
97        }
98    }
99
100    const fn get_from_packed(data: &[u8; DATA_LAYER_SIZE], index: usize) -> u8 {
101        let packed = data[index >> 1];
102        packed >> ((index & 1) << 2) & MAX_LIGHT_LEVEL
103    }
104
105    const fn set_in_packed(data: &mut [u8; DATA_LAYER_SIZE], index: usize, value: u8) {
106        let byte_index = index >> 1;
107        let shift = (index & 1) << 2;
108        let mask = !(MAX_LIGHT_LEVEL << shift);
109        let value = (value & MAX_LIGHT_LEVEL) << shift;
110        data[byte_index] = data[byte_index] & mask | value;
111    }
112
113    const fn index(x: usize, y: usize, z: usize) -> usize {
114        y << 8 | z << 4 | x
115    }
116
117    const fn pack_filled(value: u8) -> u8 {
118        let value = value & MAX_LIGHT_LEVEL;
119        value | value << 4
120    }
121}
122
123/// Chunk-owned section presence and data state.
124#[derive(Debug, Clone, PartialEq, Eq)]
125pub enum LightSection {
126    /// No light data exists for this section.
127    Missing,
128    /// Externally visible light data.
129    Visible(LightSectionData),
130    /// Internal light data omitted from vanilla packet conversion.
131    Internal(LightSectionData),
132}
133
134impl LightSection {
135    /// Creates a missing light section.
136    #[must_use]
137    pub const fn missing() -> Self {
138        Self::Missing
139    }
140
141    /// Creates a visible light section.
142    #[must_use]
143    pub const fn visible(data: LightSectionData) -> Self {
144        Self::Visible(data)
145    }
146
147    /// Creates an internal-only light section.
148    #[must_use]
149    pub const fn internal(data: LightSectionData) -> Self {
150        Self::Internal(data)
151    }
152
153    /// Returns visible data for external packet and world-light reads.
154    #[must_use]
155    pub const fn visible_data(&self) -> Option<&LightSectionData> {
156        match self {
157            Self::Visible(data) => Some(data),
158            Self::Missing | Self::Internal(_) => None,
159        }
160    }
161
162    /// Returns true when any light data is present, including internal data.
163    #[must_use]
164    pub const fn is_present(&self) -> bool {
165        matches!(self, Self::Visible(_) | Self::Internal(_))
166    }
167}
168
169/// Per-layer chunk-owned light storage.
170#[derive(Debug)]
171pub struct ChunkLightLayerStorage {
172    layer: LightLayer,
173    range: LightSectionRange,
174    chunk_section_count: usize,
175    sections: Box<[LightSection]>,
176    emptiness_map: Option<Box<[bool]>>,
177}
178
179impl ChunkLightLayerStorage {
180    /// Creates missing light sections for every light section in a chunk.
181    #[must_use]
182    pub fn new(layer: LightLayer, range: LightSectionRange, chunk_section_count: usize) -> Self {
183        let sections = (0..range.section_count())
184            .map(|_| LightSection::missing())
185            .collect();
186        Self {
187            layer,
188            range,
189            chunk_section_count,
190            sections,
191            emptiness_map: None,
192        }
193    }
194
195    /// Returns this storage's light layer.
196    #[must_use]
197    pub const fn layer(&self) -> LightLayer {
198        self.layer
199    }
200
201    /// Returns the vertical light-section range.
202    #[must_use]
203    pub const fn range(&self) -> LightSectionRange {
204        self.range
205    }
206
207    /// Returns all chunk light sections.
208    #[must_use]
209    pub fn sections(&self) -> &[LightSection] {
210        &self.sections
211    }
212
213    /// Returns all chunk light sections mutably.
214    #[must_use]
215    pub fn sections_mut(&mut self) -> &mut [LightSection] {
216        &mut self.sections
217    }
218
219    /// Returns the number of real chunk sections tracked by the emptiness map.
220    #[must_use]
221    pub const fn chunk_section_count(&self) -> usize {
222        self.chunk_section_count
223    }
224
225    /// Returns a light section for a section Y coordinate.
226    #[must_use]
227    pub fn section(&self, section_y: i32) -> Option<&LightSection> {
228        self.range
229            .section_index(section_y)
230            .and_then(|index| self.sections.get(index))
231    }
232
233    /// Returns a mutable light section for a section Y coordinate.
234    pub fn section_mut(&mut self, section_y: i32) -> Option<&mut LightSection> {
235        let index = self.range.section_index(section_y)?;
236        self.sections.get_mut(index)
237    }
238
239    /// Returns the current section emptiness map, if known.
240    #[must_use]
241    pub fn emptiness_map(&self) -> Option<&[bool]> {
242        self.emptiness_map.as_deref()
243    }
244
245    /// Returns the known emptiness for one real chunk section Y.
246    #[must_use]
247    pub fn section_empty(&self, section_y: i32) -> Option<bool> {
248        let index = self.chunk_section_index(section_y)?;
249        self.emptiness_map
250            .as_deref()
251            .and_then(|emptiness_map| emptiness_map.get(index).copied())
252    }
253
254    /// Returns the highest real chunk section known to contain blocks.
255    #[must_use]
256    pub(crate) fn highest_non_empty_section_y(&self) -> Option<i32> {
257        let emptiness_map = self.emptiness_map.as_deref()?;
258        for (index, empty) in emptiness_map.iter().copied().enumerate().rev() {
259            if !empty {
260                return self.range.chunk_section_y(index);
261            }
262        }
263
264        None
265    }
266
267    /// Replaces the section emptiness map.
268    pub fn set_emptiness_map(
269        &mut self,
270        emptiness_map: Box<[bool]>,
271    ) -> Result<(), ChunkLightEmptinessMapLengthError> {
272        let actual = emptiness_map.len();
273        if actual != self.chunk_section_count {
274            return Err(ChunkLightEmptinessMapLengthError {
275                expected: self.chunk_section_count,
276                actual,
277            });
278        }
279
280        self.emptiness_map = Some(emptiness_map);
281        Ok(())
282    }
283
284    /// Replaces the section emptiness map from current section counters.
285    pub fn refresh_emptiness_map_from_sections(
286        &mut self,
287        sections: &Sections,
288    ) -> Result<(), ChunkLightEmptinessMapLengthError> {
289        self.set_emptiness_map(sections.section_emptiness_map())
290    }
291
292    /// Updates one known section emptiness entry, returning the previous value.
293    pub fn set_section_empty(&mut self, section_y: i32, empty: bool) -> Option<bool> {
294        let index = self.chunk_section_index(section_y)?;
295        let emptiness_map = self.emptiness_map.as_deref_mut()?;
296        let previous = *emptiness_map.get(index)?;
297        emptiness_map[index] = empty;
298        Some(previous)
299    }
300
301    /// Applies `ScalableLux`'s loaded-sky-data normalization.
302    pub(crate) fn fill_loaded_missing_sky_sections_below_data_with_zero(&mut self) {
303        if self.layer != LightLayer::Sky {
304            return;
305        }
306
307        let mut below_loaded_data = false;
308        for section in self.sections.iter_mut().rev() {
309            if section.is_present() {
310                below_loaded_data = true;
311                continue;
312            }
313
314            if below_loaded_data {
315                *section = LightSection::visible(LightSectionData::homogeneous(0));
316            }
317        }
318    }
319
320    /// Returns the visible light value for one block position.
321    #[must_use]
322    pub fn get_light_value(&self, block_pos: BlockPos) -> u8 {
323        match self.layer {
324            LightLayer::Sky => self.get_sky_light_value(block_pos),
325            LightLayer::Block => self.get_block_light_value(block_pos),
326        }
327    }
328
329    fn get_block_light_value(&self, block_pos: BlockPos) -> u8 {
330        self.visible_section_value(block_pos).unwrap_or(0)
331    }
332
333    fn get_sky_light_value(&self, block_pos: BlockPos) -> u8 {
334        if let Some(value) = self.visible_section_value(block_pos) {
335            return value;
336        }
337
338        let section_y = SectionPos::block_to_section_coord(block_pos.y());
339        let Some(highest_non_empty_section_y) = self.highest_non_empty_section_y() else {
340            return MAX_LIGHT_LEVEL;
341        };
342        if section_y > highest_non_empty_section_y {
343            return MAX_LIGHT_LEVEL;
344        }
345
346        let local_x = section_relative_coord(block_pos.x());
347        let local_z = section_relative_coord(block_pos.z());
348        let mut search_section_y = section_y.saturating_add(1).max(self.range.min_section_y());
349        while search_section_y < self.range.max_section_y_exclusive() {
350            if let Some(section) = self.section(search_section_y)
351                && let Some(data) = section.visible_data()
352            {
353                return data.get(local_x, 0, local_z);
354            }
355            search_section_y += 1;
356        }
357
358        MAX_LIGHT_LEVEL
359    }
360
361    fn visible_section_value(&self, block_pos: BlockPos) -> Option<u8> {
362        let section_y = SectionPos::block_to_section_coord(block_pos.y());
363        let section = self.section(section_y)?;
364        let data = section.visible_data()?;
365
366        Some(data.get(
367            section_relative_coord(block_pos.x()),
368            section_relative_coord(block_pos.y()),
369            section_relative_coord(block_pos.z()),
370        ))
371    }
372
373    fn chunk_section_index(&self, section_y: i32) -> Option<usize> {
374        let index = self.range.chunk_section_index(section_y)?;
375        (index < self.chunk_section_count).then_some(index)
376    }
377}
378
379const fn section_relative_coord(block_coord: i32) -> usize {
380    (block_coord & 15) as usize
381}
382
383/// Chunk-owned block and sky light storage.
384#[derive(Debug)]
385pub struct ChunkLightData {
386    /// Block light sections and section emptiness metadata.
387    pub block: ChunkLightLayerStorage,
388    /// Sky light sections and section emptiness metadata.
389    pub sky: ChunkLightLayerStorage,
390}
391
392impl ChunkLightData {
393    /// Creates empty light storage for one chunk.
394    pub fn new(min_y: i32, height: i32) -> Result<Self, LightSectionRangeError> {
395        let range = LightSectionRange::from_world_height(min_y, height)?;
396        Ok(Self {
397            block: ChunkLightLayerStorage::new(
398                LightLayer::Block,
399                range,
400                range.chunk_section_count(),
401            ),
402            sky: ChunkLightLayerStorage::new(LightLayer::Sky, range, range.chunk_section_count()),
403        })
404    }
405
406    /// Creates storage for world heights already accepted by chunk construction.
407    ///
408    /// Invalid world heights are fatal because chunk-owned light arrays cannot
409    /// be indexed coherently without the vanilla padded light-section range.
410    ///
411    /// # Panics
412    ///
413    /// Panics when the supplied world height cannot form a valid light-section range.
414    #[must_use]
415    pub fn for_valid_world_height(min_y: i32, height: i32) -> Self {
416        match Self::new(min_y, height) {
417            Ok(data) => data,
418            Err(error) => panic!("invalid world height for chunk light data: {error:?}"),
419        }
420    }
421
422    /// Refreshes both layer emptiness maps from current chunk section counters.
423    pub fn refresh_emptiness_maps_from_sections(
424        &mut self,
425        sections: &Sections,
426    ) -> Result<(), ChunkLightEmptinessMapLengthError> {
427        self.block.refresh_emptiness_map_from_sections(sections)?;
428        self.sky.refresh_emptiness_map_from_sections(sections)
429    }
430
431    /// Updates one real chunk section's known emptiness in both light layers.
432    pub fn set_section_empty(&mut self, section_y: i32, empty: bool) -> bool {
433        let block_changed = self
434            .block
435            .set_section_empty(section_y, empty)
436            .is_some_and(|previous| previous != empty);
437        let sky_changed = self
438            .sky
439            .set_section_empty(section_y, empty)
440            .is_some_and(|previous| previous != empty);
441        block_changed || sky_changed
442    }
443
444    /// Returns the visible light value for one layer at a block position.
445    #[must_use]
446    pub fn get_light_value(&self, layer: LightLayer, block_pos: BlockPos) -> u8 {
447        match layer {
448            LightLayer::Sky => self.sky.get_light_value(block_pos),
449            LightLayer::Block => self.block.get_light_value(block_pos),
450        }
451    }
452}
453
454#[cfg(test)]
455mod tests {
456    use super::*;
457
458    #[test]
459    fn sky_light_below_range_starts_search_at_min_light_section() {
460        let Ok(range) = LightSectionRange::from_world_height(0, 16) else {
461            panic!("valid test height should create a light section range");
462        };
463        let mut storage =
464            ChunkLightLayerStorage::new(LightLayer::Sky, range, range.chunk_section_count());
465        let Some(section) = storage.section_mut(range.min_section_y()) else {
466            panic!("test range should include its minimum light section");
467        };
468        *section = LightSection::visible(LightSectionData::homogeneous(7));
469        let Ok(()) = storage.set_emptiness_map(vec![false; range.chunk_section_count()].into())
470        else {
471            panic!("test emptiness map should match the range");
472        };
473
474        assert_eq!(storage.get_light_value(BlockPos::new(0, i32::MIN, 0)), 7);
475    }
476}