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 MONSTERS_BURN_ATTRIBUTE: &str = "minecraft:gameplay/monsters_burn";
13const DEFAULT_SKY_LIGHT_LEVEL: f32 = 15.0;
14const DEFAULT_SUN_ANGLE: f32 = 0.0;
15const MIN_SKY_LIGHT_LEVEL: f32 = 0.0;
16const MAX_SKY_LIGHT_LEVEL: f32 = 15.0;
17const RAIN_SKY_LIGHT_TARGET: f32 = 4.0;
18const RAIN_SKY_LIGHT_ALPHA: f32 = 0.3125;
19const THUNDER_SKY_LIGHT_TARGET: f32 = 4.0;
20const THUNDER_SKY_LIGHT_ALPHA: f32 = 0.527_343_75;
21
22#[must_use]
23pub(super) fn sky_light_level(
24 dimension_type: DimensionTypeRef,
25 clock_manager: &WorldClockManager,
26 rain_level: f32,
27 thunder_level: f32,
28 can_have_weather: bool,
29) -> f32 {
30 let mut value = dimension_type
31 .sky_light_level
32 .unwrap_or(DEFAULT_SKY_LIGHT_LEVEL);
33 value = apply_timeline_float_attribute(
34 value,
35 dimension_type,
36 clock_manager,
37 SKY_LIGHT_LEVEL_ATTRIBUTE,
38 );
39 if can_have_weather {
40 value = apply_weather_sky_light_level(value, rain_level, thunder_level);
41 }
42 value.clamp(MIN_SKY_LIGHT_LEVEL, MAX_SKY_LIGHT_LEVEL)
43}
44
45#[must_use]
46pub(super) fn sun_angle_degrees(
47 dimension_type: DimensionTypeRef,
48 clock_manager: &WorldClockManager,
49) -> f32 {
50 apply_timeline_float_attribute(
51 DEFAULT_SUN_ANGLE,
52 dimension_type,
53 clock_manager,
54 SUN_ANGLE_ATTRIBUTE,
55 )
56}
57
58#[must_use]
59pub(super) fn sky_darkening(sky_light_level: f32) -> u8 {
60 (MAX_SKY_LIGHT_LEVEL - sky_light_level.clamp(MIN_SKY_LIGHT_LEVEL, MAX_SKY_LIGHT_LEVEL)) as u8
61}
62
63fn apply_timeline_float_attribute(
64 mut value: f32,
65 dimension_type: DimensionTypeRef,
66 clock_manager: &WorldClockManager,
67 attribute: &str,
68) -> f32 {
69 for_each_timeline(dimension_type, |timeline| {
70 value = apply_timeline_float_track(value, timeline, clock_manager, attribute);
71 });
72 value
73}
74
75#[must_use]
77pub(super) fn monsters_burn(
78 dimension_type: DimensionTypeRef,
79 clock_manager: &WorldClockManager,
80) -> bool {
81 let mut value = false;
82 for_each_timeline(dimension_type, |timeline| {
83 value = apply_timeline_bool_track(value, timeline, clock_manager, MONSTERS_BURN_ATTRIBUTE);
84 });
85 value
86}
87
88fn for_each_timeline(dimension_type: DimensionTypeRef, mut visit: impl FnMut(TimelineRef)) {
89 let Some(timelines) = dimension_type.timelines else {
90 return;
91 };
92 if let Some(tag) = timelines.strip_prefix('#') {
93 let Ok(tag) = Identifier::from_str(tag) else {
94 return;
95 };
96 for timeline in REGISTRY.timelines.iter_tag(&tag) {
97 visit(timeline);
98 }
99 return;
100 }
101
102 let Ok(key) = Identifier::from_str(timelines) else {
103 return;
104 };
105 if let Some(timeline) = REGISTRY.timelines.by_key(&key) {
106 visit(timeline);
107 }
108}
109
110fn apply_timeline_bool_track(
111 value: bool,
112 timeline: TimelineRef,
113 clock_manager: &WorldClockManager,
114 attribute: &str,
115) -> bool {
116 let Some(track) = timeline.tracks.iter().find(|track| track.name == attribute) else {
117 return value;
118 };
119 let Some(total_ticks) = clock_manager.total_ticks(timeline.clock) else {
120 return value;
121 };
122 let Some(sample) = sample_bool_track(track, timeline.period_ticks.map(i64::from), total_ticks)
123 else {
124 return value;
125 };
126 match track.modifier {
127 Some("or") => value || sample,
128 Some("and") => value && sample,
129 None => sample,
130 _ => value,
131 }
132}
133
134fn sample_bool_track(track: &Track, period_ticks: Option<i64>, ticks: i64) -> Option<bool> {
137 let keyframes = track.keyframes;
138 match keyframes.len() {
139 0 => return None,
140 1 => return keyframe_bool_value(&keyframes[0].value),
141 _ => {}
142 }
143
144 let sample_ticks = period_ticks.map_or(ticks, |period| ticks.rem_euclid(period));
145 let first = &keyframes[0];
146 let last = &keyframes[keyframes.len() - 1];
147
148 if let Some(period) = period_ticks
149 && sample_ticks < first.ticks
150 {
151 return step_bool_segment(
152 last.ticks - period,
153 &last.value,
154 first.ticks,
155 &first.value,
156 sample_ticks,
157 );
158 }
159
160 for segment in keyframes.windows(2) {
161 let from = &segment[0];
162 let to = &segment[1];
163 if sample_ticks < to.ticks {
164 return step_bool_segment(from.ticks, &from.value, to.ticks, &to.value, sample_ticks);
165 }
166 }
167
168 if let Some(period) = period_ticks {
169 return step_bool_segment(
170 last.ticks,
171 &last.value,
172 first.ticks + period,
173 &first.value,
174 sample_ticks,
175 );
176 }
177
178 keyframe_bool_value(&last.value)
179}
180
181fn step_bool_segment(
182 from_ticks: i64,
183 from_value: &KeyframeValue,
184 to_ticks: i64,
185 to_value: &KeyframeValue,
186 sample_ticks: i64,
187) -> Option<bool> {
188 let from = keyframe_bool_value(from_value)?;
189 let to = keyframe_bool_value(to_value)?;
190 if sample_ticks <= from_ticks {
191 return Some(from);
192 }
193 if sample_ticks >= to_ticks {
194 return Some(to);
195 }
196 Some(from)
197}
198
199fn apply_timeline_float_track(
200 value: f32,
201 timeline: TimelineRef,
202 clock_manager: &WorldClockManager,
203 attribute: &str,
204) -> f32 {
205 let Some(track) = timeline.tracks.iter().find(|track| track.name == attribute) else {
206 return value;
207 };
208 let Some(total_ticks) = clock_manager.total_ticks(timeline.clock) else {
209 return value;
210 };
211 let Some(sample) = sample_float_track(track, timeline.period_ticks.map(i64::from), total_ticks)
212 else {
213 return value;
214 };
215 match track.modifier {
216 Some("multiply") => value * sample,
217 None => sample,
218 _ => value,
219 }
220}
221
222fn sample_float_track(track: &Track, period_ticks: Option<i64>, ticks: i64) -> Option<f32> {
223 let keyframes = track.keyframes;
224 match keyframes.len() {
225 0 => return None,
226 1 => return keyframe_float_value(&keyframes[0].value),
227 _ => {}
228 }
229
230 let sample_ticks = period_ticks.map_or(ticks, |period| ticks.rem_euclid(period));
231 let first = &keyframes[0];
232 let last = &keyframes[keyframes.len() - 1];
233
234 if let Some(period) = period_ticks
235 && sample_ticks < first.ticks
236 {
237 return interpolate_float_segment(
238 track,
239 last.ticks - period,
240 &last.value,
241 first.ticks,
242 &first.value,
243 sample_ticks,
244 );
245 }
246
247 for segment in keyframes.windows(2) {
248 let from = &segment[0];
249 let to = &segment[1];
250 if sample_ticks < to.ticks {
251 return interpolate_float_segment(
252 track,
253 from.ticks,
254 &from.value,
255 to.ticks,
256 &to.value,
257 sample_ticks,
258 );
259 }
260 }
261
262 if let Some(period) = period_ticks {
263 return interpolate_float_segment(
264 track,
265 last.ticks,
266 &last.value,
267 first.ticks + period,
268 &first.value,
269 sample_ticks,
270 );
271 }
272
273 keyframe_float_value(&last.value)
274}
275
276fn interpolate_float_segment(
277 track: &Track,
278 from_ticks: i64,
279 from_value: &KeyframeValue,
280 to_ticks: i64,
281 to_value: &KeyframeValue,
282 sample_ticks: i64,
283) -> Option<f32> {
284 let from = keyframe_float_value(from_value)?;
285 let to = keyframe_float_value(to_value)?;
286 if sample_ticks <= from_ticks {
287 return Some(from);
288 }
289 if sample_ticks >= to_ticks {
290 return Some(to);
291 }
292
293 let alpha = (sample_ticks - from_ticks) as f32 / (to_ticks - from_ticks) as f32;
294 let eased_alpha = apply_easing(track.ease.as_ref(), alpha)?;
295 Some(from + eased_alpha * (to - from))
296}
297
298fn apply_easing(ease: Option<&Ease>, alpha: f32) -> Option<f32> {
299 match ease {
300 None | Some(Ease::Named("linear")) => Some(alpha),
301 Some(Ease::Named("constant")) => Some(0.0),
302 Some(Ease::CubicBezier([x1, y1, x2, y2])) => Some(cubic_bezier(alpha, *x1, *y1, *x2, *y2)),
303 Some(Ease::Named(_)) => None,
304 }
305}
306
307fn cubic_bezier(x: f32, x1: f32, y1: f32, x2: f32, y2: f32) -> f32 {
308 let x_curve = CubicCurve::from_controls(x1, x2);
309 let y_curve = CubicCurve::from_controls(y1, y2);
310 y_curve.sample(x_curve.solve_t(x))
311}
312
313#[derive(Clone, Copy)]
314struct CubicCurve {
315 a: f32,
316 b: f32,
317 c: f32,
318}
319
320impl CubicCurve {
321 const ERROR_EPSILON: f32 = 1.0E-5;
322
323 fn from_controls(first: f32, second: f32) -> Self {
324 Self {
325 a: (3.0 * first - 3.0 * second) + 1.0,
326 b: -6.0 * first + 3.0 * second,
327 c: 3.0 * first,
328 }
329 }
330
331 fn sample(self, t: f32) -> f32 {
332 ((self.a * t + self.b) * t + self.c) * t
333 }
334
335 fn sample_gradient(self, t: f32) -> f32 {
336 (3.0 * self.a * t + 2.0 * self.b) * t + self.c
337 }
338
339 fn solve_t(self, x: f32) -> f32 {
340 let mut t = x;
341 for _ in 0..4 {
342 let error = self.sample(t) - x;
343 if error.abs() < Self::ERROR_EPSILON {
344 return t;
345 }
346 let gradient = self.sample_gradient(t);
347 if gradient < Self::ERROR_EPSILON {
348 break;
349 }
350 t -= (error / gradient).clamp(-0.25, 0.25);
351 }
352 self.solve_t_bisect(x, t)
353 }
354
355 #[expect(
356 clippy::manual_midpoint,
357 reason = "evaluation order mirrors vanilla CubicBezier.solveTBisect float arithmetic"
358 )]
359 fn solve_t_bisect(self, x: f32, initial_t: f32) -> f32 {
360 let mut lower = 0.0;
361 let mut upper = 1.0;
362 let mut t = initial_t;
363 while lower < upper {
364 let error = self.sample(t) - x;
365 if error.abs() < Self::ERROR_EPSILON {
366 return t;
367 }
368 if error < 0.0 {
369 lower = t;
370 } else {
371 upper = t;
372 }
373 t = (upper + lower) / 2.0;
374 }
375 t
376 }
377}
378
379const fn keyframe_float_value(value: &KeyframeValue) -> Option<f32> {
380 match value {
381 KeyframeValue::Float(value) => Some(*value),
382 _ => None,
383 }
384}
385
386const fn keyframe_bool_value(value: &KeyframeValue) -> Option<bool> {
387 match value {
388 KeyframeValue::Bool(value) => Some(*value),
389 _ => None,
390 }
391}
392
393fn apply_weather_sky_light_level(mut value: f32, rain_level: f32, thunder_level: f32) -> f32 {
394 let thunder_level = thunder_level.clamp(0.0, 1.0);
395 let rain_level = (rain_level - thunder_level).clamp(0.0, 1.0);
396 if rain_level > 0.0 {
397 let rain_value = lerp(RAIN_SKY_LIGHT_ALPHA, value, RAIN_SKY_LIGHT_TARGET);
398 value = lerp(rain_level, value, rain_value);
399 }
400 if thunder_level > 0.0 {
401 let thunder_value = lerp(THUNDER_SKY_LIGHT_ALPHA, value, THUNDER_SKY_LIGHT_TARGET);
402 value = lerp(thunder_level, value, thunder_value);
403 }
404 value
405}
406
407fn lerp(alpha: f32, from: f32, to: f32) -> f32 {
408 from + alpha * (to - from)
409}
410
411#[cfg(test)]
412mod tests {
413 use steel_registry::init_vanilla_registry;
414 use steel_registry::vanilla_dimension_types::{OVERWORLD, THE_NETHER};
415 use steel_registry::vanilla_world_clocks;
416
417 use super::*;
418
419 const F32_CLOSE_EPSILON: f32 = 0.000_001;
420 const OVERWORLD_WAKE_UP_TICKS: i64 = 0;
421 const OVERWORLD_DAY_TICKS: i64 = 1_000;
422 const OVERWORLD_NOON_TICKS: i64 = 6_000;
423 const OVERWORLD_SUNSET_TICKS: i64 = 12_000;
424 const OVERWORLD_SUNSET_INTERPOLATION_TICKS: i64 = 12_768;
425 const OVERWORLD_MIDNIGHT_TICKS: i64 = 18_000;
426
427 fn assert_f32_close(left: f32, right: f32) {
428 assert!(
429 (left - right).abs() < F32_CLOSE_EPSILON,
430 "left={left}, right={right}"
431 );
432 }
433
434 fn clock_manager_at(total_ticks: i64) -> WorldClockManager {
435 let mut manager = WorldClockManager::new();
436 assert_eq!(
437 manager.set_total_ticks(&vanilla_world_clocks::OVERWORLD, total_ticks),
438 Some(())
439 );
440 manager
441 }
442
443 #[test]
444 fn overworld_sky_light_uses_generated_day_timeline() {
445 init_vanilla_registry();
446
447 assert_f32_close(
448 sky_light_level(
449 &OVERWORLD,
450 &clock_manager_at(OVERWORLD_DAY_TICKS),
451 0.0,
452 0.0,
453 true,
454 ),
455 15.0,
456 );
457 assert_f32_close(
458 sky_light_level(
459 &OVERWORLD,
460 &clock_manager_at(OVERWORLD_NOON_TICKS),
461 0.0,
462 0.0,
463 true,
464 ),
465 15.0,
466 );
467 assert_f32_close(
468 sky_light_level(
469 &OVERWORLD,
470 &clock_manager_at(OVERWORLD_MIDNIGHT_TICKS),
471 0.0,
472 0.0,
473 true,
474 ),
475 4.0,
476 );
477 }
478
479 #[test]
480 fn overworld_sky_light_interpolates_sunset_from_generated_keyframes() {
481 init_vanilla_registry();
482
483 assert_f32_close(
484 sky_light_level(
485 &OVERWORLD,
486 &clock_manager_at(OVERWORLD_SUNSET_INTERPOLATION_TICKS),
487 0.0,
488 0.0,
489 true,
490 ),
491 9.503_051,
492 );
493 }
494
495 #[test]
496 fn overworld_sun_angle_uses_vanilla_cubic_bezier_easing() {
497 init_vanilla_registry();
498
499 assert_f32_close(
500 sun_angle_degrees(&OVERWORLD, &clock_manager_at(OVERWORLD_WAKE_UP_TICKS)),
501 282.374_33,
502 );
503 assert_f32_close(
504 sun_angle_degrees(&OVERWORLD, &clock_manager_at(OVERWORLD_SUNSET_TICKS)),
505 77.625_66,
506 );
507 assert_f32_close(
508 sun_angle_degrees(&OVERWORLD, &clock_manager_at(OVERWORLD_MIDNIGHT_TICKS)),
509 180.0,
510 );
511 }
512
513 #[test]
514 fn sky_light_level_applies_vanilla_weather_alpha_layers() {
515 init_vanilla_registry();
516
517 assert_f32_close(
518 sky_light_level(
519 &OVERWORLD,
520 &clock_manager_at(OVERWORLD_NOON_TICKS),
521 1.0,
522 0.0,
523 true,
524 ),
525 11.5625,
526 );
527 assert_f32_close(
528 sky_light_level(
529 &OVERWORLD,
530 &clock_manager_at(OVERWORLD_NOON_TICKS),
531 1.0,
532 1.0,
533 true,
534 ),
535 9.199_219,
536 );
537 }
538
539 #[test]
540 fn fixed_nether_sky_light_uses_dimension_attribute() {
541 init_vanilla_registry();
542
543 assert_f32_close(
544 sky_light_level(
545 &THE_NETHER,
546 &clock_manager_at(OVERWORLD_NOON_TICKS),
547 0.0,
548 0.0,
549 false,
550 ),
551 4.0,
552 );
553 }
554
555 #[test]
556 fn sky_darkening_matches_vanilla_integer_cast() {
557 assert_eq!(sky_darkening(15.0), 0);
558 assert_eq!(sky_darkening(11.5625), 3);
559 assert_eq!(sky_darkening(4.0), 11);
560 }
561
562 #[test]
563 fn monsters_burn_follows_the_generated_daylight_window() {
564 init_vanilla_registry();
565
566 for ticks in [0, 6_000, 12_000, 12_541, 23_460, 23_999] {
568 assert!(
569 monsters_burn(&OVERWORLD, &clock_manager_at(ticks)),
570 "monsters_burn should hold at tick {ticks}"
571 );
572 }
573 for ticks in [12_542, 18_000, 23_000, 23_459] {
574 assert!(
575 !monsters_burn(&OVERWORLD, &clock_manager_at(ticks)),
576 "monsters_burn should be off at tick {ticks}"
577 );
578 }
579 }
580
581 #[test]
582 fn monsters_burn_snaps_at_keyframes_instead_of_interpolating() {
583 init_vanilla_registry();
584
585 assert!(!monsters_burn(&OVERWORLD, &clock_manager_at(23_459)));
586 assert!(monsters_burn(&OVERWORLD, &clock_manager_at(23_460)));
587 }
588
589 #[test]
590 fn monsters_burn_is_off_in_dimensions_without_the_day_timeline() {
591 init_vanilla_registry();
592
593 assert!(!monsters_burn(&THE_NETHER, &clock_manager_at(6_000)));
594 }
595}