Skip to main content

steel_core/chunk/light/
propagation.rs

1use steel_registry::{blocks::block_state_ext::BlockStateExt, vanilla_blocks};
2use steel_utils::{BlockPos, BlockStateId, ChunkPos, Direction, SectionPos};
3
4use super::{
5    CachedLightBlock, LIGHT_BLOCKED, LightAxisDirection, LightCacheLayout, LightDirectionSet,
6    LightLayer, LightLayerEdit, LightQueueFlags, LightSectionEmptinessChange,
7    LightSectionReadCache, LightWorkset, MAX_LIGHT_LEVEL, PackedLightPropagationQueues,
8    PackedLightQueueEntry, PooledPackedLightQueues, get_light_block_into, get_light_opacity,
9    light_occlusion_shape,
10};
11
12/// Error returned when a block-light propagation context is built from mismatched caches.
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub enum BlockLightPropagationContextError {
15    /// Block-light propagation requires a block light edit cache.
16    WrongLayer {
17        /// Layer supplied by the edit cache.
18        layer: LightLayer,
19    },
20    /// Section and light caches were built from different cache layouts.
21    LayoutMismatch {
22        /// Layout used by the section cache.
23        section_layout: Box<LightCacheLayout>,
24        /// Layout used by the light cache.
25        light_layout: Box<LightCacheLayout>,
26    },
27    /// The workset does not contain its center chunk.
28    MissingCenterChunk {
29        /// Missing center chunk position.
30        chunk_pos: ChunkPos,
31    },
32}
33
34impl BlockLightPropagationContextError {
35    fn layout_mismatch(section_layout: LightCacheLayout, light_layout: LightCacheLayout) -> Self {
36        Self::LayoutMismatch {
37            section_layout: Box::new(section_layout),
38            light_layout: Box::new(light_layout),
39        }
40    }
41}
42
43/// Sections whose visible block-light data changed during a scoped update.
44#[derive(Debug, Clone, PartialEq, Eq)]
45pub struct BlockLightUpdateResult {
46    /// Light sections that should be reported to the world/chunk update layer.
47    pub updated_sections: Vec<SectionPos>,
48}
49
50/// Whether chunk block-light generation must validate edge consistency.
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub enum BlockLightChunkEdgeChecks {
53    /// Seed sources and validate this chunk's horizontal edges against neighbors.
54    Required,
55    /// Trust existing neighboring light and pull initialized edge levels inward.
56    Skipped,
57}
58
59/// Runs ScalableLux-style block-light propagation for changed blocks in a scoped workset.
60///
61/// This is the block-light equivalent of `ScalableLux` `propagateBlockChanges`
62/// plus publishing edited sections. It assumes the caller already created a
63/// cache window around the affected chunk and will deliver returned section
64/// updates to the world/chunk notification layer.
65pub fn propagate_block_light_changes(
66    workset: &LightWorkset,
67    positions: impl IntoIterator<Item = BlockPos>,
68) -> Result<BlockLightUpdateResult, BlockLightPropagationContextError> {
69    propagate_block_light_changes_with_empty_sections(workset, positions, [])
70}
71
72/// Runs block-light propagation after applying real section emptiness transitions.
73pub fn propagate_block_light_changes_with_empty_sections(
74    workset: &LightWorkset,
75    positions: impl IntoIterator<Item = BlockPos>,
76    empty_sections: impl IntoIterator<Item = LightSectionEmptinessChange>,
77) -> Result<BlockLightUpdateResult, BlockLightPropagationContextError> {
78    let empty_sections = empty_sections.into_iter().collect::<Vec<_>>();
79
80    workset.with_chunk_read_cache(|chunk_cache| {
81        let layout = chunk_cache.layout();
82        // ScalableLux drops queued dynamic changes once the center chunk leaves the light cache.
83        let Some(center_slot) = layout.cached_chunk(layout.center_chunk()) else {
84            return Ok(BlockLightUpdateResult {
85                updated_sections: Vec::new(),
86            });
87        };
88        if chunk_cache.chunk(center_slot).is_none() {
89            return Ok(BlockLightUpdateResult {
90                updated_sections: Vec::new(),
91            });
92        }
93
94        chunk_cache.with_section_read_cache(|section_cache| {
95            chunk_cache.with_light_edit(LightLayer::Block, |mut light_edit| {
96                let mut queues = PooledPackedLightQueues::take();
97
98                {
99                    apply_block_empty_section_changes(
100                        section_cache,
101                        &mut light_edit,
102                        &empty_sections,
103                    );
104                    let mut context = BlockLightPropagationContext::new(
105                        section_cache,
106                        &mut light_edit,
107                        &mut queues,
108                    )?;
109                    for position in positions {
110                        context.check_block(position);
111                    }
112                    context.perform_light_decrease();
113                }
114
115                let mut updated_sections = Vec::new();
116                light_edit.commit(None, |section_pos| updated_sections.push(section_pos));
117                Ok(BlockLightUpdateResult { updated_sections })
118            })
119        })
120    })
121}
122
123fn apply_block_empty_section_changes(
124    sections: &LightSectionReadCache<'_>,
125    light: &mut LightLayerEdit<'_>,
126    empty_sections: &[LightSectionEmptinessChange],
127) -> usize {
128    let mut changed_chunks = Vec::new();
129    for change in empty_sections {
130        light.set_section_empty(change.section_pos, change.empty);
131        let chunk_pos = ChunkPos::new(change.section_pos.x(), change.section_pos.z());
132        if !changed_chunks.contains(&chunk_pos) {
133            changed_chunks.push(chunk_pos);
134        }
135    }
136
137    let mut initialized = 0;
138    for chunk_pos in changed_chunks {
139        initialized += sync_block_empty_light_sections(sections, light, chunk_pos);
140    }
141    initialized
142}
143
144/// Seeds and propagates block light for the center chunk of a scoped workset.
145///
146/// This matches `ScalableLux` `BlockStarLightEngine.lightChunk`: source blocks in
147/// the center chunk are seeded, then the caller chooses between validating edge
148/// consistency or pulling already-initialized neighbor levels inward.
149pub fn propagate_block_light_chunk(
150    workset: &LightWorkset,
151    edge_checks: BlockLightChunkEdgeChecks,
152) -> Result<BlockLightUpdateResult, BlockLightPropagationContextError> {
153    workset.with_chunk_read_cache(|chunk_cache| {
154        let layout = chunk_cache.layout();
155        let Some(center_slot) = layout.cached_chunk(layout.center_chunk()) else {
156            return Err(BlockLightPropagationContextError::MissingCenterChunk {
157                chunk_pos: layout.center_chunk(),
158            });
159        };
160        let Some(center_chunk) = chunk_cache.chunk(center_slot) else {
161            return Err(BlockLightPropagationContextError::MissingCenterChunk {
162                chunk_pos: layout.center_chunk(),
163            });
164        };
165        let sources = center_chunk.block_light_sources();
166
167        chunk_cache.with_section_read_cache(|section_cache| {
168            chunk_cache.with_light_edit(LightLayer::Block, |mut light_edit| {
169                let mut queues = PooledPackedLightQueues::take();
170
171                {
172                    light_edit.reset_chunk_sections_to_missing(layout.center_chunk());
173                    sync_block_empty_light_sections(
174                        section_cache,
175                        &mut light_edit,
176                        layout.center_chunk(),
177                    );
178                    let mut context = BlockLightPropagationContext::new(
179                        section_cache,
180                        &mut light_edit,
181                        &mut queues,
182                    )?;
183                    context.seed_block_light_sources(sources);
184                    match edge_checks {
185                        BlockLightChunkEdgeChecks::Required => {
186                            context.perform_light_increase();
187                            context.check_chunk_edges(layout.center_chunk());
188                        }
189                        BlockLightChunkEdgeChecks::Skipped => {
190                            context.propagate_neighbor_levels(layout.center_chunk());
191                            context.perform_light_increase();
192                        }
193                    }
194                }
195
196                let mut updated_sections = Vec::new();
197                light_edit.commit(None, |section_pos| updated_sections.push(section_pos));
198                Ok(BlockLightUpdateResult { updated_sections })
199            })
200        })
201    })
202}
203
204/// Force-synchronizes block-light sections for an already-lit loaded chunk.
205///
206/// This matches the block layer of `ScalableLux` `forceLoadInChunk`: existing
207/// light data is kept, empty-section state is synchronized, and dirty visible
208/// sections are published before the later edge-check pass.
209pub fn force_load_block_light_chunk(
210    workset: &LightWorkset,
211) -> Result<BlockLightUpdateResult, BlockLightPropagationContextError> {
212    workset.with_chunk_read_cache(|chunk_cache| {
213        let layout = ensure_center_chunk(chunk_cache)?;
214
215        chunk_cache.with_section_read_cache(|section_cache| {
216            chunk_cache.with_light_edit(LightLayer::Block, |mut light_edit| {
217                sync_block_empty_light_sections(
218                    section_cache,
219                    &mut light_edit,
220                    layout.center_chunk(),
221                );
222
223                let mut updated_sections = Vec::new();
224                light_edit.commit(None, |section_pos| updated_sections.push(section_pos));
225                Ok(BlockLightUpdateResult { updated_sections })
226            })
227        })
228    })
229}
230
231/// Validates already-loaded block-light chunk edges without resetting sections.
232///
233/// This matches `ScalableLux` `checkBlockEdges`: the force-load pass has already
234/// synchronized empty-section state, so this pass only checks horizontal
235/// consistency against loaded neighbors and publishes its own dirty sections.
236pub fn check_block_light_chunk_edges(
237    workset: &LightWorkset,
238) -> Result<BlockLightUpdateResult, BlockLightPropagationContextError> {
239    workset.with_chunk_read_cache(|chunk_cache| {
240        let layout = ensure_center_chunk(chunk_cache)?;
241
242        chunk_cache.with_section_read_cache(|section_cache| {
243            chunk_cache.with_light_edit(LightLayer::Block, |mut light_edit| {
244                let mut queues = PooledPackedLightQueues::take();
245
246                {
247                    let mut context = BlockLightPropagationContext::new(
248                        section_cache,
249                        &mut light_edit,
250                        &mut queues,
251                    )?;
252                    context.check_chunk_edges(layout.center_chunk());
253                }
254
255                let mut updated_sections = Vec::new();
256                light_edit.commit(None, |section_pos| updated_sections.push(section_pos));
257                Ok(BlockLightUpdateResult { updated_sections })
258            })
259        })
260    })
261}
262
263/// Loads already-persisted block light and validates chunk edges without resetting sections.
264///
265/// This is the complete block-layer `lit == true` path: force-load
266/// empty-section state first, then run the edge-check pass.
267pub fn load_block_light_chunk(
268    workset: &LightWorkset,
269) -> Result<BlockLightUpdateResult, BlockLightPropagationContextError> {
270    let mut updated_sections = force_load_block_light_chunk(workset)?.updated_sections;
271    updated_sections.extend(check_block_light_chunk_edges(workset)?.updated_sections);
272    Ok(BlockLightUpdateResult { updated_sections })
273}
274
275fn ensure_center_chunk(
276    chunk_cache: &super::LightChunkReadCache<'_>,
277) -> Result<LightCacheLayout, BlockLightPropagationContextError> {
278    let layout = chunk_cache.layout();
279    let Some(center_slot) = layout.cached_chunk(layout.center_chunk()) else {
280        return Err(BlockLightPropagationContextError::MissingCenterChunk {
281            chunk_pos: layout.center_chunk(),
282        });
283    };
284    if chunk_cache.chunk(center_slot).is_none() {
285        return Err(BlockLightPropagationContextError::MissingCenterChunk {
286            chunk_pos: layout.center_chunk(),
287        });
288    }
289
290    Ok(layout)
291}
292
293fn sync_block_empty_light_sections(
294    sections: &LightSectionReadCache<'_>,
295    light: &mut LightLayerEdit<'_>,
296    chunk_pos: ChunkPos,
297) -> usize {
298    let layout = sections.layout();
299    let mut initialized = 0;
300
301    for section_y in
302        (layout.range().min_chunk_section_y()..layout.range().max_chunk_section_y_exclusive()).rev()
303    {
304        let section_pos = SectionPos::new(chunk_pos.0.x, section_y, chunk_pos.0.y);
305        if !section_is_non_empty(sections, light, section_pos) {
306            continue;
307        }
308
309        for offset_z in -1..=1 {
310            for offset_x in -1..=1 {
311                for offset_y in (-1..=1).rev() {
312                    let target = SectionPos::new(
313                        chunk_pos.0.x + offset_x,
314                        section_y + offset_y,
315                        chunk_pos.0.y + offset_z,
316                    );
317                    if light.set_section_non_missing(target) {
318                        initialized += 1;
319                    }
320                }
321            }
322        }
323    }
324
325    for offset_z in -1..=1 {
326        for offset_x in -1..=1 {
327            let target_chunk = ChunkPos::new(chunk_pos.0.x + offset_x, chunk_pos.0.y + offset_z);
328
329            for section_y in
330                (layout.range().min_section_y()..layout.range().max_section_y_exclusive()).rev()
331            {
332                let section_pos = SectionPos::new(target_chunk.0.x, section_y, target_chunk.0.y);
333                match section_neighborhood_all_empty_if_known(sections, target_chunk, section_y) {
334                    Some(true) => {
335                        light.set_section_internal(section_pos);
336                    }
337                    Some(false) => {
338                        if light.set_section_non_missing(section_pos) {
339                            initialized += 1;
340                        }
341                    }
342                    None => {
343                        if !section_neighborhood_all_empty(sections, light, target_chunk, section_y)
344                            && light.set_section_non_missing(section_pos)
345                        {
346                            initialized += 1;
347                        }
348                    }
349                }
350            }
351        }
352    }
353
354    initialized
355}
356
357fn section_neighborhood_all_empty(
358    sections: &LightSectionReadCache<'_>,
359    light: &LightLayerEdit<'_>,
360    chunk_pos: ChunkPos,
361    section_y: i32,
362) -> bool {
363    for offset_y in -1..=1 {
364        let neighbor_y = section_y + offset_y;
365        if neighbor_y < sections.layout().range().min_chunk_section_y()
366            || neighbor_y >= sections.layout().range().max_chunk_section_y_exclusive()
367        {
368            continue;
369        }
370
371        for offset_z in -1..=1 {
372            for offset_x in -1..=1 {
373                let section_pos = SectionPos::new(
374                    chunk_pos.0.x + offset_x,
375                    neighbor_y,
376                    chunk_pos.0.y + offset_z,
377                );
378                if section_is_non_empty(sections, light, section_pos) {
379                    return false;
380                }
381            }
382        }
383    }
384
385    true
386}
387
388fn section_neighborhood_all_empty_if_known(
389    sections: &LightSectionReadCache<'_>,
390    chunk_pos: ChunkPos,
391    section_y: i32,
392) -> Option<bool> {
393    for offset_y in -1..=1 {
394        let neighbor_y = section_y + offset_y;
395        if neighbor_y < sections.layout().range().min_chunk_section_y()
396            || neighbor_y >= sections.layout().range().max_chunk_section_y_exclusive()
397        {
398            continue;
399        }
400
401        for offset_z in -1..=1 {
402            for offset_x in -1..=1 {
403                let section_pos = SectionPos::new(
404                    chunk_pos.0.x + offset_x,
405                    neighbor_y,
406                    chunk_pos.0.y + offset_z,
407                );
408                let empty = sections.section_empty(section_pos)?;
409                if !empty {
410                    return Some(false);
411                }
412            }
413        }
414    }
415
416    Some(true)
417}
418
419fn section_is_non_empty(
420    sections: &LightSectionReadCache<'_>,
421    light: &LightLayerEdit<'_>,
422    section_pos: SectionPos,
423) -> bool {
424    if let Some(empty) = sections.section_empty(section_pos) {
425        return !empty;
426    }
427
428    if let Some(empty) = light.section_empty(section_pos) {
429        return !empty;
430    }
431
432    sections.has_non_empty_section(section_pos)
433}
434
435/// ScalableLux-style block-light propagation over scoped Steel light caches.
436///
437/// This keeps the queue algorithm close to `ScalableLux` while avoiding long-lived
438/// references into chunks: the caller owns the scoped section and light caches,
439/// and this context only borrows them for one propagation pass.
440pub struct BlockLightPropagationContext<'a, 'sections, 'light> {
441    layout: LightCacheLayout,
442    sections: &'a LightSectionReadCache<'sections>,
443    light: &'a mut LightLayerEdit<'light>,
444    queues: &'a mut PackedLightPropagationQueues,
445}
446
447impl<'a, 'sections, 'light> BlockLightPropagationContext<'a, 'sections, 'light> {
448    /// Creates a block-light propagation context from matching scoped caches.
449    pub fn new(
450        sections: &'a LightSectionReadCache<'sections>,
451        light: &'a mut LightLayerEdit<'light>,
452        queues: &'a mut PackedLightPropagationQueues,
453    ) -> Result<Self, BlockLightPropagationContextError> {
454        if light.layer() != LightLayer::Block {
455            return Err(BlockLightPropagationContextError::WrongLayer {
456                layer: light.layer(),
457            });
458        }
459
460        if sections.layout() != light.layout() {
461            return Err(BlockLightPropagationContextError::layout_mismatch(
462                sections.layout(),
463                light.layout(),
464            ));
465        }
466
467        Ok(Self {
468            layout: light.layout(),
469            sections,
470            light,
471            queues,
472        })
473    }
474
475    /// Handles one block-light source/opacity change, matching `ScalableLux` `checkBlock`.
476    ///
477    /// Returns false when the changed block is outside this cache window.
478    pub fn check_block(&mut self, block_pos: BlockPos) -> bool {
479        let Some(cached_block) = self.layout.cached_block(block_pos) else {
480            return false;
481        };
482
483        let current_level = self.light.get(cached_block);
484        let block_state = self.sections.get_block_state(cached_block);
485        let emitted_level = block_state.get_light_emission() & MAX_LIGHT_LEVEL;
486
487        self.light.set(cached_block, emitted_level);
488        if emitted_level != 0 {
489            self.enqueue_increase(
490                block_pos,
491                emitted_level,
492                LightDirectionSet::all(),
493                Self::shape_flags(block_state),
494            );
495        }
496
497        self.enqueue_decrease(
498            block_pos,
499            current_level,
500            LightDirectionSet::all(),
501            LightQueueFlags::EMPTY,
502        );
503        true
504    }
505
506    /// Seeds block-light sources in `ScalableLux` local-index order.
507    pub fn seed_block_light_sources(&mut self, positions: impl IntoIterator<Item = BlockPos>) {
508        for position in positions {
509            self.seed_block_light_source(position);
510        }
511    }
512
513    /// Pulls horizontal neighbor levels into this chunk's increase queue.
514    pub fn propagate_neighbor_levels(&mut self, chunk_pos: ChunkPos) {
515        for section_y in (self.layout.range().min_section_y()
516            ..self.layout.range().max_section_y_exclusive())
517            .rev()
518        {
519            let section_pos = SectionPos::new(chunk_pos.0.x, section_y, chunk_pos.0.y);
520            if !self.light.has_non_missing_section(section_pos) {
521                continue;
522            }
523
524            for direction in LightAxisDirection::HORIZONTAL {
525                self.propagate_neighbor_level_section(chunk_pos, section_y, direction);
526            }
527        }
528    }
529
530    /// Validates this chunk's horizontal edges against cached neighbor edges.
531    ///
532    /// This mirrors `ScalableLux` `checkChunkEdges`: edge values whose calculated
533    /// level differs from the stored value are delayed, converted into regular
534    /// block checks, then resolved through the decrease queue.
535    pub fn check_chunk_edges(&mut self, chunk_pos: ChunkPos) {
536        for section_y in (self.layout.range().min_section_y()
537            ..self.layout.range().max_section_y_exclusive())
538            .rev()
539        {
540            self.check_chunk_edge(chunk_pos, section_y);
541        }
542
543        self.perform_light_decrease();
544    }
545
546    /// Calculates the block-light value that should exist at `block_pos`.
547    ///
548    /// Returns `None` when the position is outside this cache window.
549    #[must_use]
550    pub fn calculate_light_value(&self, block_pos: BlockPos, expect: u8) -> Option<u8> {
551        let cached_block = self.layout.cached_block(block_pos)?;
552        let center_state = self.sections.get_block_state(cached_block);
553        let mut level = center_state.get_light_emission() & MAX_LIGHT_LEVEL;
554
555        if level >= MAX_LIGHT_LEVEL - 1 || level > expect {
556            return Some(level);
557        }
558
559        let opacity = get_light_opacity(center_state);
560        if opacity >= MAX_LIGHT_LEVEL {
561            return Some(level);
562        }
563
564        for axis_direction in LightAxisDirection::ALL {
565            let neighbor_pos = Self::offset(block_pos, axis_direction);
566            let Some(neighbor_block) = self.layout.cached_block(neighbor_pos) else {
567                continue;
568            };
569            let neighbor_level = self.light.get(neighbor_block);
570            if neighbor_level.saturating_sub(1) <= level {
571                continue;
572            }
573
574            let neighbor_state = self.sections.get_block_state(neighbor_block);
575            let direction_from_neighbor = axis_direction.opposite().direction();
576            if get_light_block_into(
577                neighbor_state,
578                center_state,
579                direction_from_neighbor,
580                opacity,
581            ) == LIGHT_BLOCKED
582            {
583                continue;
584            }
585
586            level = level.max(neighbor_level.saturating_sub(opacity));
587            if level > expect {
588                return Some(level);
589            }
590        }
591
592        Some(level)
593    }
594
595    /// Performs queued `ScalableLux` block-light decreases, then re-propagates increases.
596    pub fn perform_light_decrease(&mut self) {
597        while let Some(entry) = self.queues.dequeue_decrease() {
598            let Some(source_block) = self.cached_block_from_entry(entry) else {
599                continue;
600            };
601            let source_state = if entry.has_sided_transparent_blocks() {
602                Some(self.sections.get_block_state(source_block))
603            } else {
604                None
605            };
606
607            for axis_direction in entry.directions().directions() {
608                let neighbor_pos = Self::offset(source_block.block_pos, axis_direction);
609                let Some(neighbor_block) = self.layout.cached_block(neighbor_pos) else {
610                    continue;
611                };
612                if !self.light.has_non_missing(neighbor_block) {
613                    continue;
614                }
615                let current_level = self.light.get(neighbor_block);
616                if current_level == 0 {
617                    continue;
618                }
619
620                let neighbor_state = self.sections.get_block_state(neighbor_block);
621                let Some((target_level, flags)) = Self::target_level(
622                    entry.level(),
623                    source_state,
624                    neighbor_state,
625                    axis_direction.direction(),
626                    true,
627                ) else {
628                    continue;
629                };
630
631                if current_level > target_level {
632                    self.enqueue_increase(
633                        neighbor_pos,
634                        current_level,
635                        LightDirectionSet::all(),
636                        flags.with(LightQueueFlags::RECHECK_LEVEL),
637                    );
638                    continue;
639                }
640
641                let emitted_light = neighbor_state.get_light_emission() & MAX_LIGHT_LEVEL;
642                if emitted_light != 0 {
643                    self.enqueue_increase(
644                        neighbor_pos,
645                        emitted_light,
646                        LightDirectionSet::all(),
647                        flags.with(LightQueueFlags::WRITE_LEVEL),
648                    );
649                }
650
651                self.light.set(neighbor_block, 0);
652                if target_level > 0 {
653                    self.enqueue_decrease(
654                        neighbor_pos,
655                        target_level,
656                        LightDirectionSet::all_except_opposite(axis_direction),
657                        flags,
658                    );
659                }
660            }
661        }
662
663        self.perform_light_increase();
664    }
665
666    /// Performs queued `ScalableLux` block-light increases.
667    pub fn perform_light_increase(&mut self) {
668        while let Some(entry) = self.queues.dequeue_increase() {
669            let Some(source_block) = self.cached_block_from_entry(entry) else {
670                continue;
671            };
672            if entry.should_recheck_level() {
673                if self.light.get(source_block) != entry.level() {
674                    continue;
675                }
676            } else if entry.should_write_level() {
677                self.light.set(source_block, entry.level());
678            }
679
680            let source_state = if entry.has_sided_transparent_blocks() {
681                Some(self.sections.get_block_state(source_block))
682            } else {
683                None
684            };
685
686            for axis_direction in entry.directions().directions() {
687                let neighbor_pos = Self::offset(source_block.block_pos, axis_direction);
688                let Some(neighbor_block) = self.layout.cached_block(neighbor_pos) else {
689                    continue;
690                };
691                if !self.light.has_non_missing(neighbor_block) {
692                    continue;
693                }
694                let current_level = self.light.get(neighbor_block);
695                if current_level >= entry.level().saturating_sub(1) {
696                    continue;
697                }
698
699                let neighbor_state = self.sections.get_block_state(neighbor_block);
700                let Some((target_level, flags)) = Self::target_level(
701                    entry.level(),
702                    source_state,
703                    neighbor_state,
704                    axis_direction.direction(),
705                    false,
706                ) else {
707                    continue;
708                };
709                if target_level <= current_level {
710                    continue;
711                }
712
713                self.light.set(neighbor_block, target_level);
714                if target_level > 1 {
715                    self.enqueue_increase(
716                        neighbor_pos,
717                        target_level,
718                        LightDirectionSet::all_except_opposite(axis_direction),
719                        flags,
720                    );
721                }
722            }
723        }
724    }
725
726    fn cached_block_from_entry(&self, entry: PackedLightQueueEntry) -> Option<CachedLightBlock> {
727        self.layout.cached_block_from_packed(entry.block_pos())
728    }
729
730    fn enqueue_decrease(
731        &mut self,
732        block_pos: BlockPos,
733        level: u8,
734        directions: LightDirectionSet,
735        flags: LightQueueFlags,
736    ) {
737        let Some(packed_pos) = self.layout.encode_block_pos(block_pos) else {
738            return;
739        };
740        self.queues
741            .enqueue_decrease(PackedLightQueueEntry::from_parts(
742                packed_pos, level, directions, flags,
743            ));
744    }
745
746    fn check_chunk_edge(&mut self, chunk_pos: ChunkPos, section_y: i32) {
747        let current_section_pos = SectionPos::new(chunk_pos.0.x, section_y, chunk_pos.0.y);
748        if !self.light.has_cached_section(current_section_pos) {
749            return;
750        }
751
752        for direction in LightAxisDirection::HORIZONTAL {
753            let (neighbor_offset_x, _, neighbor_offset_z) = direction.offset();
754            let neighbor_chunk_pos = ChunkPos::new(
755                chunk_pos.0.x + neighbor_offset_x,
756                chunk_pos.0.y + neighbor_offset_z,
757            );
758            let neighbor_section_pos =
759                SectionPos::new(neighbor_chunk_pos.0.x, section_y, neighbor_chunk_pos.0.y);
760            if !self.light.has_cached_section(neighbor_section_pos) {
761                continue;
762            }
763            if !self.light.has_light_data_section(current_section_pos)
764                && !self.light.has_light_data_section(neighbor_section_pos)
765            {
766                continue;
767            }
768
769            self.check_chunk_edge_direction(chunk_pos, neighbor_chunk_pos, section_y, direction);
770        }
771    }
772
773    fn check_chunk_edge_direction(
774        &mut self,
775        chunk_pos: ChunkPos,
776        neighbor_chunk_pos: ChunkPos,
777        section_y: i32,
778        direction: LightAxisDirection,
779    ) {
780        let (neighbor_offset_x, _, neighbor_offset_z) = direction.offset();
781        let (increment_x, increment_z, start_x, start_z) =
782            Self::current_edge_scan(chunk_pos, direction);
783        let mut center_delayed_checks = [0usize; 16 * 16];
784        let mut neighbor_delayed_checks = [0usize; 16 * 16];
785        let mut center_delayed_check_count = 0;
786        let mut neighbor_delayed_check_count = 0;
787
788        let min_y = section_y << 4;
789        let max_y = min_y | 15;
790        for y in min_y..=max_y {
791            let mut x = start_x;
792            let mut z = start_z;
793            for _ in 0..16 {
794                let current_pos = BlockPos::new(x, y, z);
795                let neighbor_pos = BlockPos::new(x + neighbor_offset_x, y, z + neighbor_offset_z);
796                let Some(current_block) = self.layout.cached_block(current_pos) else {
797                    x += increment_x;
798                    z += increment_z;
799                    continue;
800                };
801                let Some(neighbor_block) = self.layout.cached_block(neighbor_pos) else {
802                    x += increment_x;
803                    z += increment_z;
804                    continue;
805                };
806
807                let current_level = self.light.get(current_block);
808                if self
809                    .calculate_light_value(current_pos, current_level)
810                    .is_some_and(|calculated| calculated != current_level)
811                {
812                    center_delayed_checks[center_delayed_check_count] = current_block.local_index;
813                    center_delayed_check_count += 1;
814                }
815
816                let neighbor_level = self.light.get(neighbor_block);
817                if self
818                    .calculate_light_value(neighbor_pos, neighbor_level)
819                    .is_some_and(|calculated| calculated != neighbor_level)
820                {
821                    neighbor_delayed_checks[neighbor_delayed_check_count] =
822                        neighbor_block.local_index;
823                    neighbor_delayed_check_count += 1;
824                }
825
826                x += increment_x;
827                z += increment_z;
828            }
829        }
830
831        let current_chunk_offset_x = chunk_pos.0.x << 4;
832        let current_chunk_offset_z = chunk_pos.0.y << 4;
833        let neighbor_chunk_offset_x = neighbor_chunk_pos.0.x << 4;
834        let neighbor_chunk_offset_z = neighbor_chunk_pos.0.y << 4;
835        let chunk_offset_y = section_y << 4;
836        let delayed_check_count = center_delayed_check_count.max(neighbor_delayed_check_count);
837        for delayed_check_index in 0..delayed_check_count {
838            if delayed_check_index < center_delayed_check_count {
839                let local_index = center_delayed_checks[delayed_check_index];
840                self.check_block(Self::block_pos_from_local_index(
841                    current_chunk_offset_x,
842                    chunk_offset_y,
843                    current_chunk_offset_z,
844                    local_index,
845                ));
846            }
847            if delayed_check_index < neighbor_delayed_check_count {
848                let local_index = neighbor_delayed_checks[delayed_check_index];
849                self.check_block(Self::block_pos_from_local_index(
850                    neighbor_chunk_offset_x,
851                    chunk_offset_y,
852                    neighbor_chunk_offset_z,
853                    local_index,
854                ));
855            }
856        }
857    }
858
859    fn seed_block_light_source(&mut self, block_pos: BlockPos) -> bool {
860        let Some(cached_block) = self.layout.cached_block(block_pos) else {
861            return false;
862        };
863
864        let block_state = self.sections.get_block_state(cached_block);
865        let emitted_level = block_state.get_light_emission() & MAX_LIGHT_LEVEL;
866        if emitted_level <= self.light.get(cached_block) {
867            return false;
868        }
869
870        self.enqueue_increase(
871            block_pos,
872            emitted_level,
873            LightDirectionSet::all(),
874            Self::shape_flags(block_state),
875        );
876        self.light.set(cached_block, emitted_level);
877        true
878    }
879
880    fn propagate_neighbor_level_section(
881        &mut self,
882        chunk_pos: ChunkPos,
883        section_y: i32,
884        direction: LightAxisDirection,
885    ) {
886        let (neighbor_offset_x, _, neighbor_offset_z) = direction.offset();
887        let neighbor_section_pos = SectionPos::new(
888            chunk_pos.0.x + neighbor_offset_x,
889            section_y,
890            chunk_pos.0.y + neighbor_offset_z,
891        );
892        if !self.light.has_light_data_section(neighbor_section_pos) {
893            return;
894        }
895
896        let (increment_x, increment_z, start_x, start_z) =
897            Self::neighbor_edge_scan(chunk_pos, direction);
898        let directions = LightDirectionSet::only(direction.opposite());
899        let flags = LightQueueFlags::EMPTY.with(LightQueueFlags::HAS_SIDED_TRANSPARENT_BLOCKS);
900
901        let min_y = section_y << 4;
902        let max_y = min_y | 15;
903        for y in min_y..=max_y {
904            let mut x = start_x;
905            let mut z = start_z;
906            for _ in 0..16 {
907                let source_pos = BlockPos::new(x, y, z);
908                let Some(source_block) = self.layout.cached_block(source_pos) else {
909                    x += increment_x;
910                    z += increment_z;
911                    continue;
912                };
913                let level = self.light.get(source_block);
914                if level > 1 {
915                    self.enqueue_increase(source_pos, level, directions, flags);
916                }
917                x += increment_x;
918                z += increment_z;
919            }
920        }
921    }
922
923    const fn current_edge_scan(
924        chunk_pos: ChunkPos,
925        direction: LightAxisDirection,
926    ) -> (i32, i32, i32, i32) {
927        let (offset_x, _, offset_z) = direction.offset();
928        if offset_x != 0 {
929            let start_x = if offset_x < 0 {
930                chunk_pos.0.x << 4
931            } else {
932                (chunk_pos.0.x << 4) | 15
933            };
934            return (0, 1, start_x, chunk_pos.0.y << 4);
935        }
936
937        let start_z = if offset_z < 0 {
938            chunk_pos.0.y << 4
939        } else {
940            (chunk_pos.0.y << 4) | 15
941        };
942        (1, 0, chunk_pos.0.x << 4, start_z)
943    }
944
945    const fn neighbor_edge_scan(
946        chunk_pos: ChunkPos,
947        direction: LightAxisDirection,
948    ) -> (i32, i32, i32, i32) {
949        let (offset_x, _, offset_z) = direction.offset();
950        if offset_x != 0 {
951            let start_x = if offset_x < 0 {
952                (chunk_pos.0.x << 4) - 1
953            } else {
954                (chunk_pos.0.x << 4) + 16
955            };
956            return (0, 1, start_x, chunk_pos.0.y << 4);
957        }
958
959        let start_z = if offset_z < 0 {
960            (chunk_pos.0.y << 4) - 1
961        } else {
962            (chunk_pos.0.y << 4) + 16
963        };
964        (1, 0, chunk_pos.0.x << 4, start_z)
965    }
966
967    const fn block_pos_from_local_index(
968        chunk_offset_x: i32,
969        chunk_offset_y: i32,
970        chunk_offset_z: i32,
971        local_index: usize,
972    ) -> BlockPos {
973        BlockPos::new(
974            chunk_offset_x | (local_index & 15) as i32,
975            chunk_offset_y | (local_index >> 8) as i32,
976            chunk_offset_z | ((local_index >> 4) & 15) as i32,
977        )
978    }
979
980    fn enqueue_increase(
981        &mut self,
982        block_pos: BlockPos,
983        level: u8,
984        directions: LightDirectionSet,
985        flags: LightQueueFlags,
986    ) {
987        let Some(packed_pos) = self.layout.encode_block_pos(block_pos) else {
988            return;
989        };
990        self.queues
991            .enqueue_increase(PackedLightQueueEntry::from_parts(
992                packed_pos, level, directions, flags,
993            ));
994    }
995
996    fn target_level(
997        propagated_level: u8,
998        source_state: Option<BlockStateId>,
999        target_state: BlockStateId,
1000        direction: Direction,
1001        saturating: bool,
1002    ) -> Option<(u8, LightQueueFlags)> {
1003        let source_state = match source_state {
1004            Some(source_state) => source_state,
1005            None => Self::air(),
1006        };
1007        let opacity = get_light_block_into(
1008            source_state,
1009            target_state,
1010            direction,
1011            get_light_opacity(target_state),
1012        );
1013        if opacity == LIGHT_BLOCKED {
1014            return None;
1015        }
1016
1017        let target_level = if saturating {
1018            propagated_level.saturating_sub(opacity)
1019        } else if opacity >= propagated_level {
1020            return None;
1021        } else {
1022            propagated_level - opacity
1023        };
1024
1025        Some((target_level, Self::shape_flags(target_state)))
1026    }
1027
1028    fn shape_flags(block_state: BlockStateId) -> LightQueueFlags {
1029        if light_occlusion_shape(block_state).is_empty() {
1030            LightQueueFlags::EMPTY
1031        } else {
1032            LightQueueFlags::EMPTY.with(LightQueueFlags::HAS_SIDED_TRANSPARENT_BLOCKS)
1033        }
1034    }
1035
1036    const fn offset(block_pos: BlockPos, direction: LightAxisDirection) -> BlockPos {
1037        let (dx, dy, dz) = direction.offset();
1038        block_pos.offset(dx, dy, dz)
1039    }
1040
1041    fn air() -> BlockStateId {
1042        vanilla_blocks::AIR.default_state()
1043    }
1044}
1045
1046#[cfg(test)]
1047mod tests {
1048    use std::sync::{Arc, Weak};
1049
1050    use steel_registry::{
1051        blocks::properties::{BlockStateProperties, SlabType},
1052        init_vanilla_registry, vanilla_blocks,
1053    };
1054    use steel_utils::{ChunkPos, types::UpdateFlags};
1055
1056    use super::*;
1057    use crate::behavior::init_behaviors;
1058    use crate::chunk::{
1059        Chunk,
1060        chunk_holder::ChunkHolder,
1061        chunk_ticket_manager::ChunkTicketLevel,
1062        light::{LightCacheSetupRadius, LightSection, LightSectionData, LightSectionRange},
1063        section::{ChunkSection, Sections},
1064        status::ChunkStatus,
1065    };
1066
1067    fn init_tests() {
1068        init_vanilla_registry();
1069        init_behaviors();
1070    }
1071
1072    fn range() -> LightSectionRange {
1073        let Ok(range) = LightSectionRange::from_world_height(0, 16) else {
1074            panic!("test height should create a valid light range");
1075        };
1076        range
1077    }
1078
1079    fn holder_with_section(pos: ChunkPos, section: ChunkSection) -> Arc<ChunkHolder> {
1080        let sections = Sections::from_owned(vec![section].into_boxed_slice());
1081        let proto = Chunk::new(sections, pos, 0, 16, Weak::new());
1082        let holder = Arc::new(ChunkHolder::new(
1083            pos,
1084            ChunkTicketLevel::FULL_CHUNK,
1085            Some(ChunkTicketLevel::FULL_CHUNK),
1086            0,
1087            16,
1088        ));
1089        holder.insert_chunk(proto, ChunkStatus::Light);
1090        holder
1091    }
1092
1093    fn initialize_holder_light(holder: &ChunkHolder) {
1094        let Some(chunk) = holder.try_chunk(ChunkStatus::Empty) else {
1095            panic!("test chunk should be available");
1096        };
1097        chunk.initialize_light_sources();
1098    }
1099
1100    fn set_block_section_non_missing(holder: &ChunkHolder, section_y: i32) {
1101        set_block_light_section(
1102            holder,
1103            section_y,
1104            LightSection::visible(LightSectionData::homogeneous(0)),
1105        );
1106    }
1107
1108    fn set_visible_block_light(
1109        holder: &ChunkHolder,
1110        section_y: i32,
1111        x: usize,
1112        y: usize,
1113        z: usize,
1114        level: u8,
1115    ) {
1116        let mut data = LightSectionData::homogeneous(0);
1117        data.set(x, y, z, level);
1118        set_block_light_section(holder, section_y, LightSection::visible(data));
1119    }
1120
1121    fn set_block_light_section(holder: &ChunkHolder, section_y: i32, section: LightSection) {
1122        let Some(chunk) = holder.try_chunk(ChunkStatus::Empty) else {
1123            panic!("test chunk should be available");
1124        };
1125        let mut light = chunk.light_mut();
1126        let Some(target) = light.block.section_mut(section_y) else {
1127            panic!("test section should be inside light range");
1128        };
1129        *target = section;
1130    }
1131
1132    fn block_light_at(holder: &ChunkHolder, pos: BlockPos) -> u8 {
1133        let Some(chunk) = holder.try_chunk(ChunkStatus::Empty) else {
1134            panic!("test chunk should be available");
1135        };
1136        chunk.light().get_light_value(LightLayer::Block, pos)
1137    }
1138
1139    #[test]
1140    fn context_requires_block_layer() {
1141        init_tests();
1142        let center = ChunkPos::new(0, 0);
1143        let holder = holder_with_section(center, ChunkSection::new_empty());
1144        set_block_section_non_missing(&holder, 0);
1145        let layout = LightCacheLayout::new(center, range());
1146        let Ok(workset) = LightWorkset::setup(
1147            layout,
1148            LightCacheSetupRadius::Inner,
1149            true,
1150            |pos| (pos == center).then(|| Arc::clone(&holder)),
1151            |_| true,
1152        ) else {
1153            panic!("relaxed setup should accept missing neighbors");
1154        };
1155
1156        workset.with_chunk_read_cache(|chunk_cache| {
1157            chunk_cache.with_section_read_cache(|section_cache| {
1158                chunk_cache.with_light_edit(LightLayer::Sky, |mut light_edit| {
1159                    let mut queues = PooledPackedLightQueues::take();
1160                    let result = BlockLightPropagationContext::new(
1161                        section_cache,
1162                        &mut light_edit,
1163                        &mut queues,
1164                    );
1165
1166                    assert_eq!(
1167                        result.err(),
1168                        Some(BlockLightPropagationContextError::WrongLayer {
1169                            layer: LightLayer::Sky,
1170                        })
1171                    );
1172                });
1173            });
1174        });
1175    }
1176
1177    #[test]
1178    fn block_light_runner_publishes_visible_updates() {
1179        init_tests();
1180        let center = ChunkPos::new(0, 0);
1181        let source_pos = BlockPos::new(1, 1, 1);
1182        let mut section = ChunkSection::new_empty();
1183        section.set_block_state(1, 1, 1, vanilla_blocks::LIGHT.default_state());
1184        let holder = holder_with_section(center, section);
1185        set_block_section_non_missing(&holder, 0);
1186        let layout = LightCacheLayout::new(center, range());
1187        let Ok(workset) = LightWorkset::setup(
1188            layout,
1189            LightCacheSetupRadius::Inner,
1190            true,
1191            |pos| (pos == center).then(|| Arc::clone(&holder)),
1192            |_| true,
1193        ) else {
1194            panic!("relaxed setup should accept missing neighbors");
1195        };
1196
1197        let Ok(result) = propagate_block_light_changes(&workset, [source_pos]) else {
1198            panic!("matching block caches should run block light updates");
1199        };
1200
1201        assert!(result.updated_sections.contains(&SectionPos::new(0, 0, 0)));
1202        assert_eq!(block_light_at(&holder, source_pos), 15);
1203        assert_eq!(block_light_at(&holder, BlockPos::new(2, 1, 1)), 14);
1204    }
1205
1206    #[test]
1207    fn block_light_changes_apply_empty_section_transitions() {
1208        init_tests();
1209        let center = ChunkPos::new(0, 0);
1210        let removed_pos = BlockPos::new(1, 1, 1);
1211        let mut holders = Vec::new();
1212        let mut center_holder = None;
1213        for z in -2..=2 {
1214            for x in -2..=2 {
1215                let pos = ChunkPos::new(x, z);
1216                let mut section = ChunkSection::new_empty();
1217                if pos == center {
1218                    section.set_block_state(1, 1, 1, vanilla_blocks::STONE.default_state());
1219                }
1220                let holder = holder_with_section(pos, section);
1221                initialize_holder_light(&holder);
1222                if pos == center {
1223                    center_holder = Some(Arc::clone(&holder));
1224                }
1225                holders.push((pos, holder));
1226            }
1227        }
1228        let Some(center_holder) = center_holder else {
1229            panic!("center holder should be created");
1230        };
1231        set_visible_block_light(&center_holder, 0, 1, 1, 1, 9);
1232
1233        let Some(chunk) = center_holder.try_chunk(ChunkStatus::Empty) else {
1234            panic!("center chunk should be available");
1235        };
1236        assert_eq!(
1237            chunk.set_block_state_for_generation(
1238                ChunkStatus::Light,
1239                removed_pos,
1240                vanilla_blocks::AIR.default_state(),
1241                UpdateFlags::UPDATE_NONE,
1242            ),
1243            Some(vanilla_blocks::STONE.default_state())
1244        );
1245
1246        let layout = LightCacheLayout::new(center, range());
1247        let Ok(workset) = LightWorkset::setup(
1248            layout,
1249            LightCacheSetupRadius::Full,
1250            true,
1251            |pos| {
1252                holders
1253                    .iter()
1254                    .find(|(holder_pos, _)| *holder_pos == pos)
1255                    .map(|(_, holder)| Arc::clone(holder))
1256            },
1257            |_| true,
1258        ) else {
1259            panic!("relaxed setup should accept cached test chunks");
1260        };
1261
1262        let Ok(result) = propagate_block_light_changes_with_empty_sections(
1263            &workset,
1264            [removed_pos],
1265            [LightSectionEmptinessChange {
1266                section_pos: SectionPos::new(0, 0, 0),
1267                empty: true,
1268            }],
1269        ) else {
1270            panic!("matching block caches should run block light updates");
1271        };
1272
1273        assert!(result.updated_sections.contains(&SectionPos::new(0, 0, 0)));
1274        let Some(chunk) = center_holder.try_chunk(ChunkStatus::Empty) else {
1275            panic!("center chunk should be available");
1276        };
1277        let light = chunk.light();
1278        assert_eq!(light.block.section_empty(0), Some(true));
1279        assert_eq!(light.get_light_value(LightLayer::Block, removed_pos), 0);
1280        assert!(matches!(
1281            light.block.section(0),
1282            Some(LightSection::Missing | LightSection::Internal(_))
1283        ));
1284    }
1285
1286    #[test]
1287    fn block_light_chunk_seeds_center_sources() {
1288        init_tests();
1289        let center = ChunkPos::new(0, 0);
1290        let source_pos = BlockPos::new(1, 1, 1);
1291        let mut section = ChunkSection::new_empty();
1292        section.set_block_state(1, 1, 1, vanilla_blocks::LIGHT.default_state());
1293        let holder = holder_with_section(center, section);
1294        let layout = LightCacheLayout::new(center, range());
1295        let Ok(workset) = LightWorkset::setup(
1296            layout,
1297            LightCacheSetupRadius::Inner,
1298            true,
1299            |pos| (pos == center).then(|| Arc::clone(&holder)),
1300            |_| true,
1301        ) else {
1302            panic!("relaxed setup should accept missing neighbors");
1303        };
1304
1305        let Ok(result) = propagate_block_light_chunk(&workset, BlockLightChunkEdgeChecks::Skipped)
1306        else {
1307            panic!("matching block caches should run block chunk lighting");
1308        };
1309
1310        assert!(result.updated_sections.contains(&SectionPos::new(0, 0, 0)));
1311        assert_eq!(block_light_at(&holder, source_pos), 15);
1312        assert_eq!(block_light_at(&holder, BlockPos::new(2, 1, 1)), 14);
1313    }
1314
1315    #[test]
1316    fn block_light_chunk_pulls_neighbor_edge_levels() {
1317        init_tests();
1318        let center = ChunkPos::new(0, 0);
1319        let east_chunk = ChunkPos::new(1, 0);
1320        let center_holder = holder_with_section(center, ChunkSection::new_empty());
1321        let mut east_section = ChunkSection::new_empty();
1322        east_section.set_block_state(0, 1, 1, vanilla_blocks::LIGHT.default_state());
1323        let east_holder = holder_with_section(east_chunk, east_section);
1324        set_visible_block_light(&east_holder, 0, 0, 1, 1, 15);
1325        let layout = LightCacheLayout::new(center, range());
1326        let Ok(workset) = LightWorkset::setup(
1327            layout,
1328            LightCacheSetupRadius::Inner,
1329            true,
1330            |pos| {
1331                if pos == center {
1332                    Some(Arc::clone(&center_holder))
1333                } else if pos == east_chunk {
1334                    Some(Arc::clone(&east_holder))
1335                } else {
1336                    None
1337                }
1338            },
1339            |_| true,
1340        ) else {
1341            panic!("relaxed setup should accept missing neighbors");
1342        };
1343
1344        let Ok(result) = propagate_block_light_chunk(&workset, BlockLightChunkEdgeChecks::Skipped)
1345        else {
1346            panic!("matching block caches should run block chunk lighting");
1347        };
1348
1349        assert!(result.updated_sections.contains(&SectionPos::new(0, 0, 0)));
1350        assert_eq!(block_light_at(&center_holder, BlockPos::new(15, 1, 1)), 14);
1351        assert_eq!(block_light_at(&center_holder, BlockPos::new(14, 1, 1)), 13);
1352    }
1353
1354    #[test]
1355    fn block_light_chunk_requires_center_chunk() {
1356        init_tests();
1357        let center = ChunkPos::new(0, 0);
1358        let layout = LightCacheLayout::new(center, range());
1359        let Ok(workset) = LightWorkset::setup(
1360            layout,
1361            LightCacheSetupRadius::Inner,
1362            true,
1363            |_| None,
1364            |_| true,
1365        ) else {
1366            panic!("relaxed setup should accept missing chunks");
1367        };
1368
1369        assert_eq!(
1370            propagate_block_light_chunk(&workset, BlockLightChunkEdgeChecks::Skipped).err(),
1371            Some(BlockLightPropagationContextError::MissingCenterChunk { chunk_pos: center })
1372        );
1373    }
1374
1375    #[test]
1376    fn block_light_changes_skip_missing_center_chunk() {
1377        init_tests();
1378        let center = ChunkPos::new(0, 0);
1379        let layout = LightCacheLayout::new(center, range());
1380        let Ok(workset) = LightWorkset::setup(
1381            layout,
1382            LightCacheSetupRadius::Full,
1383            true,
1384            |_| None,
1385            |_| true,
1386        ) else {
1387            panic!("relaxed setup should accept missing chunks");
1388        };
1389
1390        let Ok(result) = propagate_block_light_changes_with_empty_sections(
1391            &workset,
1392            [BlockPos::new(1, 1, 1)],
1393            [LightSectionEmptinessChange {
1394                section_pos: SectionPos::new(0, 0, 0),
1395                empty: true,
1396            }],
1397        ) else {
1398            panic!("dynamic block changes should skip a missing center chunk");
1399        };
1400
1401        assert_eq!(result.updated_sections.len(), 0);
1402    }
1403
1404    #[test]
1405    fn block_light_calculation_respects_occluding_faces() {
1406        init_tests();
1407        let center = ChunkPos::new(0, 0);
1408        let mut section = ChunkSection::new_empty();
1409        let bottom_slab = vanilla_blocks::STONE_SLAB
1410            .default_state()
1411            .set_value(&BlockStateProperties::SLAB_TYPE, SlabType::Bottom);
1412        section.set_block_state(1, 1, 1, bottom_slab);
1413        let holder = holder_with_section(center, section);
1414        set_block_section_non_missing(&holder, 0);
1415        let layout = LightCacheLayout::new(center, range());
1416        let Ok(workset) = LightWorkset::setup(
1417            layout,
1418            LightCacheSetupRadius::Inner,
1419            true,
1420            |pos| (pos == center).then(|| Arc::clone(&holder)),
1421            |_| true,
1422        ) else {
1423            panic!("relaxed setup should accept missing neighbors");
1424        };
1425
1426        workset.with_chunk_read_cache(|chunk_cache| {
1427            chunk_cache.with_section_read_cache(|section_cache| {
1428                chunk_cache.with_light_edit(LightLayer::Block, |mut light_edit| {
1429                    let mut queues = PooledPackedLightQueues::take();
1430                    let Ok(context) = BlockLightPropagationContext::new(
1431                        section_cache,
1432                        &mut light_edit,
1433                        &mut queues,
1434                    ) else {
1435                        panic!("matching block caches should build a propagation context");
1436                    };
1437                    let Some(below) = layout.cached_block(BlockPos::new(1, 0, 1)) else {
1438                        panic!("below neighbor should be cached");
1439                    };
1440                    assert!(context.light.set(below, 15));
1441
1442                    assert_eq!(
1443                        context.calculate_light_value(BlockPos::new(1, 1, 1), 0),
1444                        Some(0)
1445                    );
1446                });
1447            });
1448        });
1449    }
1450}