1use steel_registry::blocks::{block_state_ext::BlockStateExt, shapes::VoxelShape};
4use steel_utils::{BlockStateId, Direction, SectionPos};
5
6use crate::physics::shapes::{face_shape_occludes, merged_face_occludes};
7
8pub const MAX_LIGHT_LEVEL: u8 = 15;
10pub const MIN_LIGHT_OPACITY: u8 = 1;
12pub const LIGHT_BLOCKED: u8 = MAX_LIGHT_LEVEL + 1;
14pub const LIGHT_SECTION_PADDING: i32 = 1;
16
17pub const DATA_LAYER_EDGE: usize = 16;
19pub const DATA_LAYER_BLOCK_COUNT: usize = DATA_LAYER_EDGE * DATA_LAYER_EDGE * DATA_LAYER_EDGE;
21pub const DATA_LAYER_SIZE: usize = DATA_LAYER_BLOCK_COUNT / 2;
23const CHUNK_EDGE: usize = 16;
24const CHUNK_COLUMN_COUNT: usize = CHUNK_EDGE * CHUNK_EDGE;
25const NEGATIVE_INFINITY: i32 = i32::MIN;
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
29pub enum LightLayer {
30 Sky,
32 Block,
34}
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
38pub struct LightSectionEmptinessChange {
39 pub section_pos: SectionPos,
41 pub empty: bool,
43}
44
45#[must_use]
47pub fn has_different_light_properties(old_state: BlockStateId, new_state: BlockStateId) -> bool {
48 old_state != new_state
49 && (old_state.get_light_dampening() != new_state.get_light_dampening()
50 || old_state.get_light_emission() != new_state.get_light_emission()
51 || old_state.use_shape_for_light_occlusion()
52 || new_state.use_shape_for_light_occlusion())
53}
54
55#[must_use]
60pub fn get_light_opacity(state: BlockStateId) -> u8 {
61 state.get_light_dampening().max(MIN_LIGHT_OPACITY)
62}
63
64#[must_use]
66pub fn light_occlusion_shape(state: BlockStateId) -> VoxelShape {
67 if !state.get_block().config.can_occlude || !state.use_shape_for_light_occlusion() {
68 return VoxelShape::EMPTY;
69 }
70
71 state.get_occlusion_shape()
72}
73
74#[must_use]
76pub fn get_light_block_into(
77 from_state: BlockStateId,
78 to_state: BlockStateId,
79 direction: Direction,
80 simple_opacity: u8,
81) -> u8 {
82 let from_shape = light_occlusion_shape(from_state);
83 let to_shape = light_occlusion_shape(to_state);
84 if from_shape.is_empty() && to_shape.is_empty() {
85 return simple_opacity;
86 }
87
88 if merged_face_occludes(from_shape, to_shape, direction) {
89 LIGHT_BLOCKED
90 } else {
91 simple_opacity
92 }
93}
94
95#[must_use]
97pub fn light_face_occludes(
98 from_state: BlockStateId,
99 to_state: BlockStateId,
100 direction: Direction,
101) -> bool {
102 let from_shape = light_occlusion_shape(from_state);
103 let to_shape = light_occlusion_shape(to_state);
104 face_shape_occludes(from_shape, direction, to_shape, direction.opposite())
105}
106
107mod cache;
108mod data_layer;
109mod packet;
110mod propagation;
111mod queue;
112mod section_storage;
113mod sky_propagation;
114mod sky_sources;
115mod storage;
116mod work_gate;
117mod workset;
118
119pub use cache::{
120 CachedLightBlock, CachedLightChunk, CachedLightSection, LIGHT_CACHE_CHUNK_SLOTS,
121 LIGHT_CACHE_DIAMETER, LIGHT_CACHE_RADIUS, LIGHT_CACHE_SECTION_RADIUS, LightCacheChunkScope,
122 LightCacheLayout, LightCacheSetupChunks, LightCacheSetupRadius, LightChunkSectionSlots,
123 LightChunkSlotArray, LightSectionSlotArray, LightUpdateNotificationCache, PackedLightBlockPos,
124};
125pub use data_layer::{DataLayer, DataLayerLengthError};
126pub use packet::{build_chunk_light_update_packet, build_chunk_light_update_packet_for_sections};
127pub use propagation::{
128 BlockLightChunkEdgeChecks, BlockLightPropagationContext, BlockLightPropagationContextError,
129 BlockLightUpdateResult, check_block_light_chunk_edges, force_load_block_light_chunk,
130 load_block_light_chunk, propagate_block_light_changes,
131 propagate_block_light_changes_with_empty_sections, propagate_block_light_chunk,
132};
133pub use queue::{
134 LightAxisDirection, LightDirectionSet, LightDirectionSetIter, LightPropagationQueue,
135 LightPropagationQueues, LightQueueEntry, LightQueueFlags, PackedLightPropagationQueue,
136 PackedLightPropagationQueues, PackedLightQueueEntry, PooledPackedLightQueues,
137 QueuedLightUpdate,
138};
139pub use section_storage::{LightSectionRange, LightSectionRangeError};
140pub use sky_propagation::{
141 SkyLightChunkEdgeChecks, SkyLightPropagationContext, SkyLightPropagationContextError,
142 SkyLightUpdateResult, check_sky_light_chunk_edges, force_load_sky_light_chunk,
143 load_sky_light_chunk, propagate_sky_light_changes,
144 propagate_sky_light_changes_with_empty_sections, propagate_sky_light_chunk,
145 propagate_sky_light_chunk_without_edge_checks,
146};
147pub use sky_sources::ChunkSkyLightSources;
148pub use storage::{
149 ChunkLightData, ChunkLightEmptinessMapLengthError, ChunkLightLayerStorage, LightSection,
150 LightSectionData,
151};
152pub(crate) use work_gate::{LightWorkWindowGate, LightWorkWindowReservation};
153pub use workset::{
154 LightChunkReadCache, LightLayerEdit, LightSectionReadCache, LightWorkset,
155 LightWorksetSetupError,
156};
157
158#[cfg(test)]
159mod tests {
160 use steel_registry::{
161 blocks::{block_state_ext::BlockStateExt, properties::BlockStateProperties},
162 init_vanilla_registry, vanilla_blocks,
163 };
164 use steel_utils::BlockStateId;
165 use steel_utils::{BlockPos, ChunkPos, SectionPos};
166
167 use crate::{
168 behavior::init_behaviors,
169 chunk::section::{ChunkSection, Sections},
170 };
171
172 use super::{
173 ChunkLightData, ChunkSkyLightSources, DATA_LAYER_SIZE, DataLayer, LightLayer, LightSection,
174 LightSectionData, LightSectionRange, MAX_LIGHT_LEVEL, build_chunk_light_update_packet,
175 build_chunk_light_update_packet_for_sections, get_light_opacity,
176 has_different_light_properties,
177 };
178
179 fn init_light_tests() {
180 init_vanilla_registry();
181 init_behaviors();
182 }
183
184 fn empty_sections(section_count: usize) -> Sections {
185 let sections: Vec<ChunkSection> = (0..section_count)
186 .map(|_| ChunkSection::new_empty())
187 .collect();
188 Sections::from_owned(sections.into_boxed_slice())
189 }
190
191 fn single_section_with_block(local_y: usize, state: BlockStateId) -> Sections {
192 let mut section = ChunkSection::new_empty();
193 section.set_block_state(0, local_y, 0, state);
194 Sections::from_owned(vec![section].into_boxed_slice())
195 }
196
197 fn new_test_sky_sources() -> ChunkSkyLightSources {
198 let Ok(sources) = ChunkSkyLightSources::new(0, 16) else {
199 panic!("valid single-section height rejected");
200 };
201 sources
202 }
203
204 fn mask_bit(mask: &[u64], index: usize) -> bool {
205 (mask[index / 64] & (1 << (index % 64))) != 0
206 }
207
208 #[test]
209 fn data_layer_uses_vanilla_low_nibble_first_order() {
210 let mut layer = DataLayer::new();
211
212 layer.set(0, 0, 0, 5);
213 layer.set(1, 0, 0, 12);
214 layer.set(1, 2, 3, 31);
215
216 assert_eq!(layer.get(0, 0, 0), 5);
217 assert_eq!(layer.get(1, 0, 0), 12);
218 assert_eq!(layer.get(1, 2, 3), MAX_LIGHT_LEVEL);
219 assert_eq!(layer.get(2, 2, 3), 0);
220
221 let bytes = layer.to_bytes();
222 assert_eq!(bytes[0], 0xC5);
223 }
224
225 #[test]
226 fn data_layer_preserves_homogeneous_non_zero_without_backing_bytes() {
227 let layer = DataLayer::filled(15);
228
229 assert!(layer.is_homogeneous());
230 assert!(!layer.is_empty());
231 assert_eq!(layer.homogeneous_value(), Some(15));
232 assert!(layer.to_bytes().iter().all(|byte| *byte == 0xFF));
233 }
234
235 #[test]
236 fn light_section_range_matches_vanilla_padded_section_range() {
237 let range = LightSectionRange::from_world_height(-64, 384)
238 .expect("vanilla overworld height should produce a light range");
239
240 assert_eq!(range.min_section_y(), -5);
241 assert_eq!(range.max_section_y_exclusive(), 21);
242 assert_eq!(range.section_count(), 26);
243 assert_eq!(range.chunk_section_count(), 24);
244 assert_eq!(range.section_index(-5), Some(0));
245 assert_eq!(range.section_y(25), Some(20));
246 assert_eq!(range.section_index(21), None);
247 }
248
249 #[test]
250 fn chunk_light_packet_omits_missing_and_internal_sections() {
251 let mut light = ChunkLightData::for_valid_world_height(0, 16);
252 *light.sky.section_mut(0).expect("real section in range") =
253 LightSection::internal(LightSectionData::homogeneous(15));
254 *light.block.section_mut(0).expect("real section in range") = LightSection::missing();
255
256 let packet = build_chunk_light_update_packet(&light, true);
257
258 assert!(!mask_bit(&packet.sky_y_mask.0, 1));
259 assert!(!mask_bit(&packet.empty_sky_y_mask.0, 1));
260 assert_eq!(packet.sky_updates.len(), 0);
261 assert!(!mask_bit(&packet.block_y_mask.0, 1));
262 assert!(!mask_bit(&packet.empty_block_y_mask.0, 1));
263 assert_eq!(packet.block_updates.len(), 0);
264 }
265
266 #[test]
267 fn chunk_light_packet_uses_empty_mask_for_visible_zero_sections() {
268 let mut light = ChunkLightData::for_valid_world_height(0, 16);
269 *light.block.section_mut(0).expect("real section in range") =
270 LightSection::visible(LightSectionData::homogeneous(0));
271
272 let packet = build_chunk_light_update_packet(&light, true);
273
274 assert!(!mask_bit(&packet.block_y_mask.0, 1));
275 assert!(mask_bit(&packet.empty_block_y_mask.0, 1));
276 assert_eq!(packet.block_updates.len(), 0);
277 }
278
279 #[test]
280 fn chunk_light_packet_expands_visible_homogeneous_non_zero_sections() {
281 let mut light = ChunkLightData::for_valid_world_height(0, 16);
282 *light.sky.section_mut(0).expect("real section in range") =
283 LightSection::visible(LightSectionData::homogeneous(15));
284
285 let packet = build_chunk_light_update_packet(&light, true);
286
287 assert!(mask_bit(&packet.sky_y_mask.0, 1));
288 assert!(!mask_bit(&packet.empty_sky_y_mask.0, 1));
289 assert_eq!(packet.sky_updates.len(), 1);
290 assert_eq!(packet.sky_updates[0].len(), DATA_LAYER_SIZE);
291 assert!(packet.sky_updates[0].iter().all(|byte| *byte == 0xFF));
292 }
293
294 #[test]
295 fn chunk_light_packet_omits_sky_layer_when_dimension_has_no_skylight() {
296 let mut light = ChunkLightData::for_valid_world_height(0, 16);
297 *light.sky.section_mut(0).expect("real section in range") =
298 LightSection::visible(LightSectionData::homogeneous(15));
299
300 let packet = build_chunk_light_update_packet(&light, false);
301
302 assert_eq!(packet.sky_updates.len(), 0);
303 assert!(!mask_bit(&packet.sky_y_mask.0, 1));
304 assert!(!mask_bit(&packet.empty_sky_y_mask.0, 1));
305 }
306
307 #[test]
308 fn changed_section_packet_preserves_ascending_light_section_order() {
309 let chunk_pos = ChunkPos::new(3, -2);
310 let mut light = ChunkLightData::for_valid_world_height(0, 48);
311 *light.block.section_mut(2).expect("upper section in range") =
312 LightSection::visible(LightSectionData::homogeneous(3));
313 *light.block.section_mut(0).expect("lower section in range") =
314 LightSection::visible(LightSectionData::homogeneous(7));
315
316 let packet = build_chunk_light_update_packet_for_sections(
317 chunk_pos,
318 &light,
319 true,
320 &[],
321 &[
322 SectionPos::new(chunk_pos.0.x, 2, chunk_pos.0.y),
323 SectionPos::new(chunk_pos.0.x, 0, chunk_pos.0.y),
324 ],
325 );
326
327 assert_eq!(packet.block_updates.len(), 2);
328 assert!(packet.block_updates[0].iter().all(|byte| *byte == 0x77));
329 assert!(packet.block_updates[1].iter().all(|byte| *byte == 0x33));
330 }
331
332 #[test]
333 fn chunk_light_data_reads_visible_block_and_sky_light() {
334 let mut light = ChunkLightData::for_valid_world_height(0, 16);
335 let pos = BlockPos::new(1, 2, 3);
336 let mut data = LightSectionData::homogeneous(0);
337 data.set(1, 2, 3, 12);
338 *light.block.section_mut(0).expect("real section in range") = LightSection::visible(data);
339
340 assert_eq!(light.get_light_value(LightLayer::Block, pos), 12);
341 assert_eq!(light.get_light_value(LightLayer::Sky, pos), 15);
342 }
343
344 #[test]
345 fn sections_collect_block_light_sources_in_scalable_lux_order() {
346 init_light_tests();
347
348 let torch = vanilla_blocks::TORCH.default_state();
349 let lantern = vanilla_blocks::SEA_LANTERN.default_state();
350 let mut lower = ChunkSection::new_empty();
351 lower.set_block_state(3, 4, 5, torch);
352 lower.set_block_state(1, 0, 0, lantern);
353 let mut upper = ChunkSection::new_empty();
354 upper.set_block_state(15, 15, 15, lantern);
355 let sections =
356 Sections::from_owned(vec![lower, ChunkSection::new_empty(), upper].into_boxed_slice());
357
358 assert_eq!(
359 sections.block_light_sources(ChunkPos::new(2, -3), -16),
360 vec![
361 BlockPos::new(33, -16, -48),
362 BlockPos::new(35, -12, -43),
363 BlockPos::new(47, 31, -33),
364 ]
365 );
366 }
367
368 #[test]
369 fn light_opacity_uses_vanilla_minimum_opacity() {
370 init_light_tests();
371 let air = vanilla_blocks::AIR.default_state();
372 let stone = vanilla_blocks::STONE.default_state();
373
374 assert_eq!(get_light_opacity(air), 1);
375 assert_eq!(get_light_opacity(stone), 15);
376 }
377
378 #[test]
379 fn different_light_properties_match_vanilla_conditions() {
380 init_light_tests();
381 let air = vanilla_blocks::AIR.default_state();
382 let stone = vanilla_blocks::STONE.default_state();
383
384 assert!(!has_different_light_properties(air, air));
385 assert!(has_different_light_properties(air, stone));
386
387 let light = vanilla_blocks::LIGHT.default_state();
388 let dim_light = light.set_value(&BlockStateProperties::LEVEL, 7);
389 assert!(has_different_light_properties(light, dim_light));
390 }
391
392 #[test]
393 fn sky_light_sources_empty_chunk_extends_below_world() {
394 init_light_tests();
395 let sections = empty_sections(1);
396 let mut sources = new_test_sky_sources();
397
398 sources.fill_from_sections(§ions);
399
400 assert_eq!(sources.get_lowest_source_y(0, 0), i32::MIN);
401 assert_eq!(sources.get_lowest_source_y(15, 15), i32::MIN);
402 assert_eq!(sources.get_highest_lowest_source_y(), i32::MIN);
403 }
404
405 #[test]
406 fn sky_light_sources_find_lowest_occluding_edge() {
407 init_light_tests();
408 let stone = vanilla_blocks::STONE.default_state();
409 let sections = single_section_with_block(4, stone);
410 let mut sources = new_test_sky_sources();
411
412 sources.fill_from_sections(§ions);
413
414 assert_eq!(sources.get_lowest_source_y(0, 0), 5);
415 assert_eq!(sources.get_lowest_source_y(1, 0), i32::MIN);
416 assert_eq!(sources.get_highest_lowest_source_y(), 5);
417 }
418
419 #[test]
420 fn sky_light_sources_update_adds_and_removes_occluding_edge() {
421 init_light_tests();
422 let air = vanilla_blocks::AIR.default_state();
423 let stone = vanilla_blocks::STONE.default_state();
424 let sections = empty_sections(1);
425 let mut sources = new_test_sky_sources();
426 sources.fill_from_sections(§ions);
427
428 let added = sources.update(0, 4, 0, |_x, y, _z| if y == 4 { stone } else { air });
429
430 assert!(added);
431 assert_eq!(sources.get_lowest_source_y(0, 0), 5);
432
433 let removed = sources.update(0, 4, 0, |_x, _y, _z| air);
434
435 assert!(removed);
436 assert_eq!(sources.get_lowest_source_y(0, 0), i32::MIN);
437 }
438
439 #[test]
440 fn sky_light_sources_update_ignores_changes_below_current_source_edge() {
441 init_light_tests();
442 let stone = vanilla_blocks::STONE.default_state();
443 let sections = single_section_with_block(10, stone);
444 let mut sources = new_test_sky_sources();
445 sources.fill_from_sections(§ions);
446
447 let changed = sources.update(0, 4, 0, |_x, _y, _z| stone);
448
449 assert!(!changed);
450 assert_eq!(sources.get_lowest_source_y(0, 0), 11);
451 }
452}