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