1use std::str::FromStr as _;
2
3use steel_registry::dimension_type::DimensionTypeRef;
4use steel_registry::timeline::{Ease, KeyframeValue, TimelineRef, Track};
5use steel_registry::{REGISTRY, RegistryExt as _, TaggedRegistryExt as _};
6use steel_utils::Identifier;
7
8use super::clock::WorldClockManager;
9
10const SKY_LIGHT_LEVEL_ATTRIBUTE: &str = "minecraft:gameplay/sky_light_level";
11const SUN_ANGLE_ATTRIBUTE: &str = "minecraft:visual/sun_angle";
12const DEFAULT_SKY_LIGHT_LEVEL: f32 = 15.0;
13const DEFAULT_SUN_ANGLE: f32 = 0.0;
14const MIN_SKY_LIGHT_LEVEL: f32 = 0.0;
15const MAX_SKY_LIGHT_LEVEL: f32 = 15.0;
16const RAIN_SKY_LIGHT_TARGET: f32 = 4.0;
17const RAIN_SKY_LIGHT_ALPHA: f32 = 0.3125;
18const THUNDER_SKY_LIGHT_TARGET: f32 = 4.0;
19const THUNDER_SKY_LIGHT_ALPHA: f32 = 0.527_343_75;
20
21#[must_use]
22pub(super) fn sky_light_level(
23 dimension_type: DimensionTypeRef,
24 clock_manager: &WorldClockManager,
25 rain_level: f32,
26 thunder_level: f32,
27 can_have_weather: bool,
28) -> f32 {
29 let mut value = dimension_type
30 .sky_light_level
31 .unwrap_or(DEFAULT_SKY_LIGHT_LEVEL);
32 value = apply_timeline_float_attribute(
33 value,
34 dimension_type,
35 clock_manager,
36 SKY_LIGHT_LEVEL_ATTRIBUTE,
37 );
38 if can_have_weather {
39 value = apply_weather_sky_light_level(value, rain_level, thunder_level);
40 }
41 value.clamp(MIN_SKY_LIGHT_LEVEL, MAX_SKY_LIGHT_LEVEL)
42}
43
44#[must_use]
45pub(super) fn sun_angle_degrees(
46 dimension_type: DimensionTypeRef,
47 clock_manager: &WorldClockManager,
48) -> f32 {
49 apply_timeline_float_attribute(
50 DEFAULT_SUN_ANGLE,
51 dimension_type,
52 clock_manager,
53 SUN_ANGLE_ATTRIBUTE,
54 )
55}
56
57#[must_use]
58pub(super) fn sky_darkening(sky_light_level: f32) -> u8 {
59 (MAX_SKY_LIGHT_LEVEL - sky_light_level.clamp(MIN_SKY_LIGHT_LEVEL, MAX_SKY_LIGHT_LEVEL)) as u8
60}
61
62fn apply_timeline_float_attribute(
63 mut value: f32,
64 dimension_type: DimensionTypeRef,
65 clock_manager: &WorldClockManager,
66 attribute: &str,
67) -> f32 {
68 let Some(timelines) = dimension_type.timelines else {
69 return value;
70 };
71 if let Some(tag) = timelines.strip_prefix('#') {
72 let Ok(tag) = Identifier::from_str(tag) else {
73 return value;
74 };
75 for timeline in REGISTRY.timelines.iter_tag(&tag) {
76 value = apply_timeline_float_track(value, timeline, clock_manager, attribute);
77 }
78 return value;
79 }
80
81 let Ok(key) = Identifier::from_str(timelines) else {
82 return value;
83 };
84 REGISTRY.timelines.by_key(&key).map_or(value, |timeline| {
85 apply_timeline_float_track(value, timeline, clock_manager, attribute)
86 })
87}
88
89fn apply_timeline_float_track(
90 value: f32,
91 timeline: TimelineRef,
92 clock_manager: &WorldClockManager,
93 attribute: &str,
94) -> f32 {
95 let Some(track) = timeline.tracks.iter().find(|track| track.name == attribute) else {
96 return value;
97 };
98 let Some(total_ticks) = clock_manager.total_ticks(timeline.clock) else {
99 return value;
100 };
101 let Some(sample) = sample_float_track(track, timeline.period_ticks.map(i64::from), total_ticks)
102 else {
103 return value;
104 };
105 match track.modifier {
106 Some("multiply") => value * sample,
107 None => sample,
108 _ => value,
109 }
110}
111
112fn sample_float_track(track: &Track, period_ticks: Option<i64>, ticks: i64) -> Option<f32> {
113 let keyframes = track.keyframes;
114 match keyframes.len() {
115 0 => return None,
116 1 => return keyframe_float_value(&keyframes[0].value),
117 _ => {}
118 }
119
120 let sample_ticks = period_ticks.map_or(ticks, |period| ticks.rem_euclid(period));
121 let first = &keyframes[0];
122 let last = &keyframes[keyframes.len() - 1];
123
124 if let Some(period) = period_ticks
125 && sample_ticks < first.ticks
126 {
127 return interpolate_float_segment(
128 track,
129 last.ticks - period,
130 &last.value,
131 first.ticks,
132 &first.value,
133 sample_ticks,
134 );
135 }
136
137 for segment in keyframes.windows(2) {
138 let from = &segment[0];
139 let to = &segment[1];
140 if sample_ticks < to.ticks {
141 return interpolate_float_segment(
142 track,
143 from.ticks,
144 &from.value,
145 to.ticks,
146 &to.value,
147 sample_ticks,
148 );
149 }
150 }
151
152 if let Some(period) = period_ticks {
153 return interpolate_float_segment(
154 track,
155 last.ticks,
156 &last.value,
157 first.ticks + period,
158 &first.value,
159 sample_ticks,
160 );
161 }
162
163 keyframe_float_value(&last.value)
164}
165
166fn interpolate_float_segment(
167 track: &Track,
168 from_ticks: i64,
169 from_value: &KeyframeValue,
170 to_ticks: i64,
171 to_value: &KeyframeValue,
172 sample_ticks: i64,
173) -> Option<f32> {
174 let from = keyframe_float_value(from_value)?;
175 let to = keyframe_float_value(to_value)?;
176 if sample_ticks <= from_ticks {
177 return Some(from);
178 }
179 if sample_ticks >= to_ticks {
180 return Some(to);
181 }
182
183 let alpha = (sample_ticks - from_ticks) as f32 / (to_ticks - from_ticks) as f32;
184 let eased_alpha = apply_easing(track.ease.as_ref(), alpha)?;
185 Some(from + eased_alpha * (to - from))
186}
187
188fn apply_easing(ease: Option<&Ease>, alpha: f32) -> Option<f32> {
189 match ease {
190 None | Some(Ease::Named("linear")) => Some(alpha),
191 Some(Ease::Named("constant")) => Some(0.0),
192 Some(Ease::CubicBezier([x1, y1, x2, y2])) => Some(cubic_bezier(alpha, *x1, *y1, *x2, *y2)),
193 Some(Ease::Named(_)) => None,
194 }
195}
196
197fn cubic_bezier(x: f32, x1: f32, y1: f32, x2: f32, y2: f32) -> f32 {
198 let x_curve = CubicCurve::from_controls(x1, x2);
199 let y_curve = CubicCurve::from_controls(y1, y2);
200 y_curve.sample(x_curve.solve_t(x))
201}
202
203#[derive(Clone, Copy)]
204struct CubicCurve {
205 a: f32,
206 b: f32,
207 c: f32,
208}
209
210impl CubicCurve {
211 const ERROR_EPSILON: f32 = 1.0E-5;
212
213 fn from_controls(first: f32, second: f32) -> Self {
214 Self {
215 a: (3.0 * first - 3.0 * second) + 1.0,
216 b: -6.0 * first + 3.0 * second,
217 c: 3.0 * first,
218 }
219 }
220
221 fn sample(self, t: f32) -> f32 {
222 ((self.a * t + self.b) * t + self.c) * t
223 }
224
225 fn sample_gradient(self, t: f32) -> f32 {
226 (3.0 * self.a * t + 2.0 * self.b) * t + self.c
227 }
228
229 fn solve_t(self, x: f32) -> f32 {
230 let mut t = x;
231 for _ in 0..4 {
232 let error = self.sample(t) - x;
233 if error.abs() < Self::ERROR_EPSILON {
234 return t;
235 }
236 let gradient = self.sample_gradient(t);
237 if gradient < Self::ERROR_EPSILON {
238 break;
239 }
240 t -= (error / gradient).clamp(-0.25, 0.25);
241 }
242 self.solve_t_bisect(x, t)
243 }
244
245 #[expect(
246 clippy::manual_midpoint,
247 reason = "evaluation order mirrors vanilla CubicBezier.solveTBisect float arithmetic"
248 )]
249 fn solve_t_bisect(self, x: f32, initial_t: f32) -> f32 {
250 let mut lower = 0.0;
251 let mut upper = 1.0;
252 let mut t = initial_t;
253 while lower < upper {
254 let error = self.sample(t) - x;
255 if error.abs() < Self::ERROR_EPSILON {
256 return t;
257 }
258 if error < 0.0 {
259 lower = t;
260 } else {
261 upper = t;
262 }
263 t = (upper + lower) / 2.0;
264 }
265 t
266 }
267}
268
269const fn keyframe_float_value(value: &KeyframeValue) -> Option<f32> {
270 match value {
271 KeyframeValue::Float(value) => Some(*value),
272 _ => None,
273 }
274}
275
276fn apply_weather_sky_light_level(mut value: f32, rain_level: f32, thunder_level: f32) -> f32 {
277 let thunder_level = thunder_level.clamp(0.0, 1.0);
278 let rain_level = (rain_level - thunder_level).clamp(0.0, 1.0);
279 if rain_level > 0.0 {
280 let rain_value = lerp(RAIN_SKY_LIGHT_ALPHA, value, RAIN_SKY_LIGHT_TARGET);
281 value = lerp(rain_level, value, rain_value);
282 }
283 if thunder_level > 0.0 {
284 let thunder_value = lerp(THUNDER_SKY_LIGHT_ALPHA, value, THUNDER_SKY_LIGHT_TARGET);
285 value = lerp(thunder_level, value, thunder_value);
286 }
287 value
288}
289
290fn lerp(alpha: f32, from: f32, to: f32) -> f32 {
291 from + alpha * (to - from)
292}
293
294#[cfg(test)]
295mod tests {
296 use steel_registry::init_vanilla_registry;
297 use steel_registry::vanilla_dimension_types::{OVERWORLD, THE_NETHER};
298 use steel_registry::vanilla_world_clocks;
299
300 use super::*;
301
302 fn assert_f32_close(left: f32, right: f32) {
303 assert!(
304 (left - right).abs() < 0.000_001,
305 "left={left}, right={right}"
306 );
307 }
308
309 fn clock_manager_at(total_ticks: i64) -> WorldClockManager {
310 let mut manager = WorldClockManager::new();
311 assert_eq!(
312 manager.set_total_ticks(&vanilla_world_clocks::OVERWORLD, total_ticks),
313 Some(())
314 );
315 manager
316 }
317
318 #[test]
319 fn overworld_sky_light_uses_generated_day_timeline() {
320 init_vanilla_registry();
321
322 assert_f32_close(
323 sky_light_level(&OVERWORLD, &clock_manager_at(6000), 0.0, 0.0, true),
324 15.0,
325 );
326 assert_f32_close(
327 sky_light_level(&OVERWORLD, &clock_manager_at(18000), 0.0, 0.0, true),
328 4.0,
329 );
330 }
331
332 #[test]
333 fn overworld_sky_light_interpolates_sunset_from_generated_keyframes() {
334 init_vanilla_registry();
335
336 assert_f32_close(
337 sky_light_level(&OVERWORLD, &clock_manager_at(12_768), 0.0, 0.0, true),
338 9.503_051,
339 );
340 }
341
342 #[test]
343 fn overworld_sun_angle_uses_vanilla_cubic_bezier_easing() {
344 init_vanilla_registry();
345
346 assert_f32_close(
347 sun_angle_degrees(&OVERWORLD, &clock_manager_at(0)),
348 282.374_33,
349 );
350 assert_f32_close(
351 sun_angle_degrees(&OVERWORLD, &clock_manager_at(12_000)),
352 77.625_66,
353 );
354 assert_f32_close(
355 sun_angle_degrees(&OVERWORLD, &clock_manager_at(18_000)),
356 180.0,
357 );
358 }
359
360 #[test]
361 fn sky_light_level_applies_vanilla_weather_alpha_layers() {
362 init_vanilla_registry();
363
364 assert_f32_close(
365 sky_light_level(&OVERWORLD, &clock_manager_at(6000), 1.0, 0.0, true),
366 11.5625,
367 );
368 assert_f32_close(
369 sky_light_level(&OVERWORLD, &clock_manager_at(6000), 1.0, 1.0, true),
370 9.199_219,
371 );
372 }
373
374 #[test]
375 fn fixed_nether_sky_light_uses_dimension_attribute() {
376 init_vanilla_registry();
377
378 assert_f32_close(
379 sky_light_level(&THE_NETHER, &clock_manager_at(6000), 0.0, 0.0, false),
380 4.0,
381 );
382 }
383
384 #[test]
385 fn sky_darkening_matches_vanilla_integer_cast() {
386 assert_eq!(sky_darkening(15.0), 0);
387 assert_eq!(sky_darkening(11.5625), 3);
388 assert_eq!(sky_darkening(4.0), 11);
389 }
390}