1use steel_registry::{blocks::block_state_ext::BlockStateExt, vanilla_blocks};
2use steel_utils::{BlockStateId, Direction, SectionPos};
3
4use crate::chunk::section::Sections;
5
6use super::{
7 CHUNK_COLUMN_COUNT, CHUNK_EDGE, LightSectionRangeError, NEGATIVE_INFINITY, light_face_occludes,
8};
9
10#[derive(Debug, Clone, PartialEq, Eq)]
16pub struct ChunkSkyLightSources {
17 min_y: i32,
18 pub(super) heightmap: [i32; CHUNK_COLUMN_COUNT],
19}
20
21impl ChunkSkyLightSources {
22 pub const fn new(min_y: i32, height: i32) -> Result<Self, LightSectionRangeError> {
24 if height <= 0 || min_y.checked_sub(1).is_none() || min_y.checked_add(height).is_none() {
25 return Err(LightSectionRangeError { min_y, height });
26 }
27
28 let min_y = min_y - 1;
29 Ok(Self {
30 min_y,
31 heightmap: [min_y; CHUNK_COLUMN_COUNT],
32 })
33 }
34
35 #[must_use]
44 pub fn for_valid_world_height(min_y: i32, height: i32) -> Self {
45 match Self::new(min_y, height) {
46 Ok(sources) => sources,
47 Err(error) => panic!("invalid world height for skylight sources: {error:?}"),
48 }
49 }
50
51 pub fn fill_from_sections(&mut self, sections: &Sections) {
53 let Some(top_section_index) = sections
54 .sections
55 .iter()
56 .rposition(|section| !section.read().is_empty())
57 else {
58 self.fill(self.min_y);
59 return;
60 };
61
62 for z in 0..CHUNK_EDGE {
63 for x in 0..CHUNK_EDGE {
64 let initial_edge_y = self.find_lowest_source_y(sections, top_section_index, x, z);
65 self.set(Self::index(x, z), initial_edge_y.max(self.min_y));
66 }
67 }
68 }
69
70 pub fn update(
75 &mut self,
76 x: usize,
77 y: i32,
78 z: usize,
79 mut state_at: impl FnMut(usize, i32, usize) -> BlockStateId,
80 ) -> bool {
81 debug_assert!(x < CHUNK_EDGE);
82 debug_assert!(z < CHUNK_EDGE);
83
84 let Some(upper_edge_y) = y.checked_add(1) else {
85 return false;
86 };
87 let index = Self::index(x, z);
88 let current_lowest_source_y = self.get(index);
89 if upper_edge_y < current_lowest_source_y {
90 return false;
91 }
92
93 let top_state = state_at(x, upper_edge_y, z);
94 let middle_state = state_at(x, y, z);
95 if self.update_edge(
96 index,
97 current_lowest_source_y,
98 x,
99 z,
100 upper_edge_y,
101 top_state,
102 y,
103 middle_state,
104 &mut state_at,
105 ) {
106 return true;
107 }
108
109 let Some(bottom_y) = y.checked_sub(1) else {
110 return false;
111 };
112 let bottom_state = state_at(x, bottom_y, z);
113 self.update_edge(
114 index,
115 current_lowest_source_y,
116 x,
117 z,
118 y,
119 middle_state,
120 bottom_y,
121 bottom_state,
122 &mut state_at,
123 )
124 }
125
126 #[must_use]
128 pub const fn get_lowest_source_y(&self, x: usize, z: usize) -> i32 {
129 self.extend_sources_below_world(self.get(Self::index(x, z)))
130 }
131
132 #[must_use]
134 pub fn get_highest_lowest_source_y(&self) -> i32 {
135 let mut max_value = NEGATIVE_INFINITY;
136 for value in self.heightmap {
137 if value > max_value {
138 max_value = value;
139 }
140 }
141 self.extend_sources_below_world(max_value)
142 }
143
144 fn find_lowest_source_y(
145 &self,
146 sections: &Sections,
147 top_section_index: usize,
148 x: usize,
149 z: usize,
150 ) -> i32 {
151 let mut top_y =
152 Self::section_to_block_coord(self.section_y_from_index(top_section_index) + 1);
153 let mut bottom_y = top_y - 1;
154 let mut top_state = Self::air_state();
155
156 for section_index in (0..=top_section_index).rev() {
157 let section = sections.sections[section_index].read();
158 if section.is_empty() {
159 top_state = Self::air_state();
160 top_y = Self::section_to_block_coord(self.section_y_from_index(section_index));
161 bottom_y = top_y - 1;
162 continue;
163 }
164
165 for y in (0..CHUNK_EDGE).rev() {
166 let bottom_state = section.states.get(x, y, z);
167 if Self::is_edge_occluded(top_state, bottom_state) {
168 return top_y;
169 }
170
171 top_state = bottom_state;
172 top_y = bottom_y;
173 bottom_y -= 1;
174 }
175 }
176
177 self.min_y
178 }
179
180 #[expect(
181 clippy::too_many_arguments,
182 reason = "mirrors vanilla's updateEdge inputs without bundling temporary positions"
183 )]
184 fn update_edge(
185 &mut self,
186 index: usize,
187 old_top_edge_y: i32,
188 x: usize,
189 z: usize,
190 checked_edge_y: i32,
191 top_state: BlockStateId,
192 bottom_y: i32,
193 bottom_state: BlockStateId,
194 state_at: &mut impl FnMut(usize, i32, usize) -> BlockStateId,
195 ) -> bool {
196 if Self::is_edge_occluded(top_state, bottom_state) {
197 if checked_edge_y > old_top_edge_y {
198 self.set(index, checked_edge_y);
199 return true;
200 }
201 } else if checked_edge_y == old_top_edge_y {
202 let new_source_y =
203 self.find_lowest_source_below(x, z, bottom_y, bottom_state, state_at);
204 self.set(index, new_source_y);
205 return true;
206 }
207
208 false
209 }
210
211 fn find_lowest_source_below(
212 &self,
213 x: usize,
214 z: usize,
215 start_y: i32,
216 start_state: BlockStateId,
217 state_at: &mut impl FnMut(usize, i32, usize) -> BlockStateId,
218 ) -> i32 {
219 let mut top_y = start_y;
220 let mut top_state = start_state;
221 let Some(mut bottom_y) = start_y.checked_sub(1) else {
222 return self.min_y;
223 };
224
225 while bottom_y >= self.min_y {
226 let bottom_state = state_at(x, bottom_y, z);
227 if Self::is_edge_occluded(top_state, bottom_state) {
228 return top_y;
229 }
230
231 top_state = bottom_state;
232 top_y = bottom_y;
233 let Some(next_bottom_y) = bottom_y.checked_sub(1) else {
234 break;
235 };
236 bottom_y = next_bottom_y;
237 }
238
239 self.min_y
240 }
241
242 fn is_edge_occluded(top_state: BlockStateId, bottom_state: BlockStateId) -> bool {
243 if bottom_state.get_light_dampening() != 0 {
244 return true;
245 }
246
247 light_face_occludes(top_state, bottom_state, Direction::Down)
248 }
249
250 fn fill(&mut self, lowest_source_y: i32) {
251 self.heightmap.fill(lowest_source_y);
252 }
253
254 const fn set(&mut self, index: usize, value: i32) {
255 self.heightmap[index] = value;
256 }
257
258 const fn get(&self, index: usize) -> i32 {
259 self.heightmap[index]
260 }
261
262 const fn extend_sources_below_world(&self, value: i32) -> i32 {
263 if value == self.min_y {
264 NEGATIVE_INFINITY
265 } else {
266 value
267 }
268 }
269
270 const fn section_y_from_index(&self, section_index: usize) -> i32 {
271 SectionPos::block_to_section_coord(self.min_y + 1) + section_index as i32
272 }
273
274 const fn section_to_block_coord(section_y: i32) -> i32 {
275 section_y << 4
276 }
277
278 const fn index(x: usize, z: usize) -> usize {
279 x + z * CHUNK_EDGE
280 }
281
282 fn air_state() -> BlockStateId {
283 vanilla_blocks::AIR.default_state()
284 }
285}