Skip to main content

steel_core/chunk/light/sky_propagation/
algorithms.rs

1use super::{
2    BlockPos, BlockStateExt, ChunkPos, Direction, LIGHT_BLOCKED, LightAxisDirection,
3    LightDirectionSet, LightQueueFlags, MAX_LIGHT_LEVEL, PackedLightQueueEntry, SectionPos,
4    SkyLightChunkEdgeChecks, SkyLightPropagationContext, get_light_block_into, get_light_opacity,
5};
6
7impl SkyLightPropagationContext<'_, '_, '_> {
8    /// Runs sky chunk lighting with the selected `ScalableLux` edge-check mode.
9    pub fn light_chunk(&mut self, chunk_pos: ChunkPos, edge_checks: SkyLightChunkEdgeChecks) {
10        self.light.rewrite_missing_sections_for_skylight();
11        self.missing_section_checked.fill(false);
12
13        let min_section = self.layout.range().min_chunk_section_y();
14        let mut highest_non_empty_section = self.layout.range().max_chunk_section_y_exclusive() - 1;
15
16        loop {
17            let section_pos =
18                SectionPos::new(chunk_pos.0.x, highest_non_empty_section, chunk_pos.0.y);
19            if highest_non_empty_section != min_section - 1
20                && self.sections.has_non_empty_section(section_pos)
21            {
22                break;
23            }
24
25            self.check_missing_section(chunk_pos, highest_non_empty_section, false);
26            self.propagate_full_empty_section_edges(chunk_pos, highest_non_empty_section);
27
28            if highest_non_empty_section == min_section - 1 {
29                highest_non_empty_section -= 1;
30                break;
31            }
32            highest_non_empty_section -= 1;
33        }
34
35        if highest_non_empty_section >= min_section {
36            self.propagate_sky_sources_from_top(chunk_pos, highest_non_empty_section);
37        }
38
39        match edge_checks {
40            SkyLightChunkEdgeChecks::Required => {
41                self.perform_light_increase();
42                for section_y in
43                    (self.layout.range().min_section_y()..=highest_non_empty_section).rev()
44                {
45                    self.check_missing_section(chunk_pos, section_y, false);
46                }
47                self.check_chunk_edges(
48                    chunk_pos,
49                    self.layout.range().min_section_y(),
50                    highest_non_empty_section,
51                );
52            }
53            SkyLightChunkEdgeChecks::Skipped => {
54                for section_y in
55                    (self.layout.range().min_section_y()..=highest_non_empty_section).rev()
56                {
57                    self.check_missing_section(chunk_pos, section_y, false);
58                }
59                self.propagate_neighbor_levels(
60                    chunk_pos,
61                    self.layout.range().min_section_y(),
62                    highest_non_empty_section,
63                );
64                self.perform_light_increase();
65            }
66        }
67    }
68
69    /// Handles one sky-light opacity change, matching `ScalableLux` `checkBlock`.
70    pub fn check_block(&mut self, block_pos: BlockPos) -> bool {
71        let Some(cached_block) = self.layout.cached_block(block_pos) else {
72            return false;
73        };
74
75        let current_level = self.light.get(cached_block);
76        if current_level == MAX_LIGHT_LEVEL {
77            self.enqueue_increase(
78                block_pos,
79                current_level,
80                LightDirectionSet::all(),
81                LightQueueFlags::EMPTY.with(LightQueueFlags::HAS_SIDED_TRANSPARENT_BLOCKS),
82            );
83        } else {
84            self.light.set(cached_block, 0);
85        }
86
87        self.enqueue_decrease(
88            block_pos,
89            current_level,
90            LightDirectionSet::all(),
91            LightQueueFlags::EMPTY,
92        );
93        true
94    }
95
96    /// Handles sky-light source and opacity changes for blocks in the center chunk.
97    pub fn propagate_block_changes(&mut self, positions: &[BlockPos]) {
98        self.light.rewrite_missing_sections_for_skylight();
99        self.missing_section_checked.fill(false);
100
101        let chunk_pos = self.layout.center_chunk();
102        self.initialize_changed_sections(chunk_pos, positions);
103
104        let mut changed_column_max_y = [i32::MIN; 16 * 16];
105        for position in positions {
106            if SectionPos::block_to_section_coord(position.x()) != chunk_pos.0.x
107                || SectionPos::block_to_section_coord(position.z()) != chunk_pos.0.y
108            {
109                continue;
110            }
111
112            let index = ((position.x() & 15) | ((position.z() & 15) << 4)) as usize;
113            changed_column_max_y[index] = changed_column_max_y[index].max(position.y());
114        }
115
116        let mut delayed_increases = Vec::new();
117        let mut delayed_decreases = Vec::new();
118        for (index, max_y) in changed_column_max_y.into_iter().enumerate() {
119            if max_y == i32::MIN {
120                continue;
121            }
122
123            let x = (chunk_pos.0.x << 4) | (index as i32 & 15);
124            let z = (chunk_pos.0.y << 4) | ((index as i32 >> 4) & 15);
125            let max_propagation_y =
126                self.try_propagate_skylight_delayed(x, max_y, z, true, &mut delayed_increases);
127            self.remove_sky_sources_below(x, max_propagation_y, z, &mut delayed_decreases);
128        }
129
130        self.process_delayed_increases(&delayed_increases);
131        self.process_delayed_decreases(&delayed_decreases);
132
133        for position in positions {
134            self.check_block(*position);
135        }
136
137        self.perform_light_decrease();
138    }
139
140    /// Calculates the sky-light value that should exist at `block_pos`.
141    #[must_use]
142    pub fn calculate_light_value(&self, block_pos: BlockPos, expect: u8) -> Option<u8> {
143        if expect == MAX_LIGHT_LEVEL {
144            return Some(expect);
145        }
146
147        let cached_block = self.layout.cached_block(block_pos)?;
148        let center_state = self.sections.get_block_state(cached_block);
149        let opacity = get_light_opacity(center_state);
150        let mut level = 0;
151
152        for axis_direction in LightAxisDirection::ALL {
153            let neighbor_pos = Self::offset(block_pos, axis_direction);
154            let Some(neighbor_block) = self.layout.cached_block(neighbor_pos) else {
155                continue;
156            };
157            let neighbor_level = self.light.get(neighbor_block);
158            if neighbor_level.saturating_sub(1) <= level {
159                continue;
160            }
161
162            let neighbor_state = self.sections.get_block_state(neighbor_block);
163            if get_light_block_into(
164                neighbor_state,
165                center_state,
166                axis_direction.opposite().direction(),
167                opacity,
168            ) == LIGHT_BLOCKED
169            {
170                continue;
171            }
172
173            level = level.max(neighbor_level.saturating_sub(opacity));
174            if level > expect {
175                return Some(level);
176            }
177        }
178
179        Some(level)
180    }
181
182    pub(super) fn init_light_section(
183        &mut self,
184        section_pos: SectionPos,
185        extrude: bool,
186        init_removed: bool,
187    ) {
188        if self.layout.section_slot(section_pos).is_none()
189            || (!self.light.has_cached_section(section_pos)
190                && (!init_removed || !self.light.materialize_removed_missing_section(section_pos)))
191        {
192            return;
193        }
194        if !self.light.is_section_missing(section_pos) {
195            return;
196        }
197
198        let mut highest_non_empty_section = self.layout.range().min_section_y() - 1;
199        for section_y in (self.layout.range().min_chunk_section_y()
200            ..self.layout.range().max_chunk_section_y_exclusive())
201            .rev()
202        {
203            let candidate = SectionPos::new(section_pos.x(), section_y, section_pos.z());
204            if self.section_is_non_empty(candidate) {
205                highest_non_empty_section = section_y;
206                break;
207            }
208        }
209
210        if section_pos.y() > highest_non_empty_section {
211            self.light.set_section_non_missing(section_pos);
212            self.light.fill_section(section_pos, MAX_LIGHT_LEVEL);
213        } else if extrude {
214            self.light
215                .extrude_lower_from_first_section_above(section_pos);
216        } else {
217            self.light.set_section_non_missing(section_pos);
218        }
219    }
220
221    pub(super) fn section_is_non_empty(&self, section_pos: SectionPos) -> bool {
222        if let Some(empty) = self.sections.section_empty(section_pos) {
223            return !empty;
224        }
225
226        if let Some(empty) = self.light.section_empty(section_pos) {
227            return !empty;
228        }
229
230        self.sections.has_non_empty_section(section_pos)
231    }
232
233    pub(super) fn check_missing_section(
234        &mut self,
235        chunk_pos: ChunkPos,
236        section_y: i32,
237        extrude_initialized: bool,
238    ) -> bool {
239        let Some(section_index) = self.layout.range().section_index(section_y) else {
240            return false;
241        };
242        if self.missing_section_checked[section_index] {
243            return false;
244        }
245        self.missing_section_checked[section_index] = true;
246
247        let center_section_pos = SectionPos::new(chunk_pos.0.x, section_y, chunk_pos.0.y);
248        let mut need_init_neighbors = self.light.has_non_missing_section(center_section_pos);
249        if !need_init_neighbors {
250            'neighbor_search: for offset_z in -1..=1 {
251                for offset_x in -1..=1 {
252                    let section_pos = SectionPos::new(
253                        chunk_pos.0.x + offset_x,
254                        section_y,
255                        chunk_pos.0.y + offset_z,
256                    );
257                    if self.light.has_non_missing_section(section_pos) {
258                        need_init_neighbors = true;
259                        break 'neighbor_search;
260                    }
261                }
262            }
263        }
264
265        if need_init_neighbors {
266            for offset_z in -1..=1 {
267                for offset_x in -1..=1 {
268                    self.init_light_section(
269                        SectionPos::new(
270                            chunk_pos.0.x + offset_x,
271                            section_y,
272                            chunk_pos.0.y + offset_z,
273                        ),
274                        if (offset_x | offset_z) == 0 {
275                            extrude_initialized
276                        } else {
277                            true
278                        },
279                        true,
280                    );
281                }
282            }
283        }
284
285        need_init_neighbors
286    }
287
288    fn propagate_full_empty_section_edges(&mut self, chunk_pos: ChunkPos, section_y: i32) {
289        for direction in LightAxisDirection::HORIZONTAL {
290            let (neighbor_offset_x, _, neighbor_offset_z) = direction.offset();
291            let neighbor_section_pos = SectionPos::new(
292                chunk_pos.0.x + neighbor_offset_x,
293                section_y,
294                chunk_pos.0.y + neighbor_offset_z,
295            );
296            if !self.light.has_non_missing_section(neighbor_section_pos) {
297                continue;
298            }
299
300            let (increment_x, increment_z, start_x, start_z) =
301                Self::current_edge_scan(chunk_pos, direction);
302            let directions = LightDirectionSet::only(direction);
303            let min_y = section_y << 4;
304            let max_y = min_y | 15;
305            for y in min_y..=max_y {
306                let mut x = start_x;
307                let mut z = start_z;
308                for _ in 0..16 {
309                    self.enqueue_increase(
310                        BlockPos::new(x, y, z),
311                        MAX_LIGHT_LEVEL,
312                        directions,
313                        LightQueueFlags::EMPTY,
314                    );
315                    x += increment_x;
316                    z += increment_z;
317                }
318            }
319        }
320    }
321
322    fn propagate_sky_sources_from_top(&mut self, chunk_pos: ChunkPos, highest_section: i32) {
323        let section_min_x = chunk_pos.0.x << 4;
324        let section_min_z = chunk_pos.0.y << 4;
325        let start_y = (highest_section << 4) | 15;
326
327        for z in 0..super::super::CHUNK_EDGE {
328            for x in 0..super::super::CHUNK_EDGE {
329                self.try_propagate_skylight_inner(
330                    section_min_x + x as i32,
331                    start_y + 1,
332                    section_min_z + z as i32,
333                    false,
334                    None,
335                );
336            }
337        }
338    }
339
340    fn try_propagate_skylight_delayed(
341        &mut self,
342        x: i32,
343        y: i32,
344        z: i32,
345        extrude_initialized: bool,
346        delayed_increases: &mut Vec<PackedLightQueueEntry>,
347    ) -> i32 {
348        self.try_propagate_skylight_inner(x, y, z, extrude_initialized, Some(delayed_increases))
349    }
350
351    fn try_propagate_skylight_inner(
352        &mut self,
353        x: i32,
354        mut y: i32,
355        z: i32,
356        extrude_initialized: bool,
357        mut delayed_increases: Option<&mut Vec<PackedLightQueueEntry>>,
358    ) -> i32 {
359        if self.get_light_level_extruded(BlockPos::new(x, y + 1, z)) != MAX_LIGHT_LEVEL {
360            return y;
361        }
362
363        self.check_missing_section(
364            ChunkPos::new(
365                SectionPos::block_to_section_coord(x),
366                SectionPos::block_to_section_coord(z),
367            ),
368            SectionPos::block_to_section_coord(y),
369            extrude_initialized,
370        );
371
372        let mut above_state = self.block_state(BlockPos::new(x, y + 1, z));
373        while y >= (self.layout.range().min_section_y() << 4) {
374            if (y & 15) == 15 {
375                self.check_missing_section(
376                    ChunkPos::new(
377                        SectionPos::block_to_section_coord(x),
378                        SectionPos::block_to_section_coord(z),
379                    ),
380                    SectionPos::block_to_section_coord(y),
381                    extrude_initialized,
382                );
383            }
384
385            let current_pos = BlockPos::new(x, y, z);
386            let current_state = self.block_state(current_pos);
387            let opacity = current_state.get_light_dampening();
388            if get_light_block_into(above_state, current_state, Direction::Down, opacity)
389                == LIGHT_BLOCKED
390                || opacity > 0
391            {
392                break;
393            }
394
395            let section_pos = SectionPos::from_block_pos(current_pos);
396            if self.light.has_non_missing_section(section_pos) {
397                let Some(cached_block) = self.layout.cached_block(current_pos) else {
398                    break;
399                };
400                let increase_entry = self.enqueue_increase(
401                    current_pos,
402                    MAX_LIGHT_LEVEL,
403                    LightDirectionSet::all_except(LightAxisDirection::PositiveY),
404                    Self::shape_flags(current_state),
405                );
406                above_state = current_state;
407
408                if let Some(delayed_increases) = delayed_increases.as_deref_mut() {
409                    if let Some(entry) = increase_entry {
410                        delayed_increases.push(entry);
411                    }
412                } else {
413                    self.light.set(cached_block, MAX_LIGHT_LEVEL);
414                }
415            } else {
416                y &= !15;
417                above_state = Self::air();
418            }
419
420            y -= 1;
421        }
422
423        y
424    }
425
426    fn initialize_changed_sections(&mut self, chunk_pos: ChunkPos, positions: &[BlockPos]) {
427        let mut section_ys = Vec::new();
428        for position in positions {
429            if SectionPos::block_to_section_coord(position.x()) != chunk_pos.0.x
430                || SectionPos::block_to_section_coord(position.z()) != chunk_pos.0.y
431            {
432                continue;
433            }
434
435            let section_y = SectionPos::block_to_section_coord(position.y());
436            if !section_ys.contains(&section_y) {
437                section_ys.push(section_y);
438            }
439        }
440
441        for section_y in section_ys {
442            let section_pos = SectionPos::new(chunk_pos.0.x, section_y, chunk_pos.0.y);
443            if !self.sections.has_non_empty_section(section_pos) {
444                continue;
445            }
446
447            for offset_z in -1..=1 {
448                for offset_x in -1..=1 {
449                    for offset_y in (-1..=1).rev() {
450                        self.init_light_section(
451                            SectionPos::new(
452                                chunk_pos.0.x + offset_x,
453                                section_y + offset_y,
454                                chunk_pos.0.y + offset_z,
455                            ),
456                            true,
457                            false,
458                        );
459                    }
460                }
461            }
462        }
463    }
464
465    fn remove_sky_sources_below(
466        &mut self,
467        x: i32,
468        mut y: i32,
469        z: i32,
470        delayed_decreases: &mut Vec<PackedLightQueueEntry>,
471    ) {
472        if self.get_light_level_extruded(BlockPos::new(x, y, z)) != MAX_LIGHT_LEVEL {
473            return;
474        }
475
476        let min_y = self.layout.range().min_section_y() << 4;
477        while y >= min_y {
478            if (y & 15) == 15 {
479                self.check_missing_section(
480                    ChunkPos::new(
481                        SectionPos::block_to_section_coord(x),
482                        SectionPos::block_to_section_coord(z),
483                    ),
484                    SectionPos::block_to_section_coord(y),
485                    true,
486                );
487            }
488
489            let current_pos = BlockPos::new(x, y, z);
490            let section_pos = SectionPos::from_block_pos(current_pos);
491            if !self.light.has_non_missing_section(section_pos) {
492                y &= !15;
493                y -= 1;
494                continue;
495            }
496
497            let Some(cached_block) = self.layout.cached_block(current_pos) else {
498                break;
499            };
500            if self.light.get(cached_block) != MAX_LIGHT_LEVEL {
501                break;
502            }
503
504            if let Some(entry) = self.enqueue_decrease(
505                current_pos,
506                MAX_LIGHT_LEVEL,
507                LightDirectionSet::all_except(LightAxisDirection::PositiveY),
508                LightQueueFlags::EMPTY,
509            ) {
510                delayed_decreases.push(entry);
511            }
512            y -= 1;
513        }
514    }
515}