Skip to main content

steel_core/world/
environment.rs

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}