Skip to main content

s3lightfixes/
light_processing.rs

1use palette::{FromColor, GetHue, Hsv, IntoColor, SetHue, rgb::Srgb};
2use tes3::esp::{EditorId, LightFlags};
3
4use crate::{CustomLightData, LightConfig};
5
6// TES3 stores these values as integers, while lightfixes intentionally exposes multipliers as
7// floats. The casts are the conversion boundary between the file format and user-authored math.
8#[allow(
9    clippy::cast_possible_truncation,
10    clippy::cast_precision_loss,
11    clippy::cast_sign_loss
12)]
13fn scaled_u32(value: u32, multiplier: f32) -> u32 {
14    (value as f32 * multiplier).max(0.0) as u32
15}
16
17// Same boundary as `scaled_u32`, but TES3 light duration is signed.
18#[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
19fn scaled_i32(value: i32, multiplier: f32) -> i32 {
20    (value as f32 * multiplier) as i32
21}
22
23// RGB channels are TES3 u8 values. Multipliers are user math; clamp so exciting configs do not wrap
24// bright red into suspiciously dark red. Rendering bugs are bad enough without integer cosplay.
25#[allow(
26    clippy::cast_possible_truncation,
27    clippy::cast_precision_loss,
28    clippy::cast_sign_loss
29)]
30fn scaled_u8(value: u8, multiplier: f32) -> u8 {
31    (f32::from(value) * multiplier).clamp(0.0, 255.0) as u8
32}
33
34// User-provided fixed durations are floats to share parser machinery with multipliers, but TES3
35// stores the result as an integer duration.
36#[allow(clippy::cast_possible_truncation)]
37fn fixed_duration_to_i32(duration: f32) -> i32 {
38    duration as i32
39}
40
41/// Given a `LightData` reference from an ESP light,
42/// returns the HSV version and whether it is colored or not (for the global modifier).
43#[must_use]
44pub fn light_to_hsv(light_data: &tes3::esp::LightData) -> (Hsv, bool) {
45    let hsv = color_to_hsv(light_data.color);
46    let hue_degrees = hsv.get_hue().into_positive_degrees();
47
48    (hsv, !(14. ..=64.).contains(&hue_degrees))
49}
50
51fn color_to_hsv(color: [u8; 4]) -> Hsv {
52    let rgb: palette::rgb::Rgb = Srgb::new(color[0], color[1], color[2]).into_format();
53    Hsv::from_color(rgb)
54}
55
56fn replacement_for_light<'a>(
57    light_config: &'a LightConfig,
58    light_id: &str,
59) -> Option<&'a CustomLightData> {
60    light_config
61        .light_regexes
62        .iter()
63        .find_map(|(regex, light_data)| regex.is_match(light_id).then_some(light_data))
64}
65
66fn apply_hsv_replacement(
67    light_as_hsv: &mut Hsv,
68    replacement: &CustomLightData,
69    global_hue: f32,
70    global_saturation: f32,
71    global_value: f32,
72    use_global_fallbacks: bool,
73) {
74    if let Some(hue_mult) = replacement.hue_mult {
75        let new_hue = palette::RgbHue::from_degrees(light_as_hsv.hue.into_raw_degrees() * hue_mult);
76        light_as_hsv.set_hue(new_hue);
77    } else if let Some(fixed_hue) = replacement.hue {
78        let new_hue = palette::RgbHue::from_degrees(hue_degrees(fixed_hue));
79        light_as_hsv.set_hue(new_hue);
80    } else if use_global_fallbacks {
81        let new_hue =
82            palette::RgbHue::from_degrees(light_as_hsv.hue.into_raw_degrees() * global_hue);
83        light_as_hsv.set_hue(new_hue);
84    }
85
86    if let Some(saturation_mult) = replacement.saturation_mult {
87        light_as_hsv.saturation *= saturation_mult;
88    } else if let Some(fixed_saturation) = replacement.saturation {
89        light_as_hsv.saturation = fixed_saturation;
90    } else if use_global_fallbacks {
91        light_as_hsv.saturation *= global_saturation;
92    }
93
94    if let Some(value_mult) = replacement.value_mult {
95        light_as_hsv.value *= value_mult;
96    } else if let Some(fixed_value) = replacement.value {
97        light_as_hsv.value = fixed_value;
98    } else if use_global_fallbacks {
99        light_as_hsv.value *= global_value;
100    }
101}
102
103#[allow(clippy::cast_precision_loss)]
104fn hue_degrees(hue: u32) -> f32 {
105    hue.clamp(0, 360) as f32
106}
107
108fn apply_plain_hsv_adjustment(
109    light_as_hsv: &mut Hsv,
110    global_hue: f32,
111    global_saturation: f32,
112    global_value: f32,
113) {
114    let new_hue = palette::RgbHue::from_degrees(light_as_hsv.hue.into_raw_degrees() * global_hue);
115
116    light_as_hsv.set_hue(new_hue);
117    light_as_hsv.saturation *= global_saturation;
118    light_as_hsv.value *= global_value;
119}
120
121fn apply_rgb_multipliers(color: &mut [u8; 4], replacement: &CustomLightData) {
122    if let Some(red_mult) = replacement.red_mult {
123        color[0] = scaled_u8(color[0], red_mult);
124    }
125    if let Some(green_mult) = replacement.green_mult {
126        color[1] = scaled_u8(color[1], green_mult);
127    }
128    if let Some(blue_mult) = replacement.blue_mult {
129        color[2] = scaled_u8(color[2], blue_mult);
130    }
131}
132
133pub fn process_light(light_config: &LightConfig, light: &mut tes3::esp::Light) -> Vec<String> {
134    let original_data = light.data.clone();
135
136    if light_config.disable_negative_lights && light.data.flags.contains(LightFlags::NEGATIVE) {
137        light.data.flags.remove(LightFlags::NEGATIVE);
138        light.data.radius = 0;
139        light.data.color = [0, 0, 0, 0];
140        return light_changes(&original_data, &light.data);
141    }
142
143    if light_config.disable_flickering {
144        light
145            .data
146            .flags
147            .remove(LightFlags::FLICKER | LightFlags::FLICKER_SLOW);
148    }
149
150    if light_config.disable_pulse {
151        light
152            .data
153            .flags
154            .remove(LightFlags::PULSE | LightFlags::PULSE_SLOW);
155    }
156
157    let light_id = light.editor_id_ascii_lowercase();
158    let (mut light_as_hsv, is_colored) = light_to_hsv(&light.data);
159    let replacement_light_data = replacement_for_light(light_config, &light_id);
160
161    let (global_radius, global_hue, global_saturation, global_value) = if is_colored {
162        (
163            light_config.colored_radius,
164            light_config.colored_hue,
165            light_config.colored_saturation,
166            light_config.colored_value,
167        )
168    } else {
169        (
170            light_config.standard_radius,
171            light_config.standard_hue,
172            light_config.standard_saturation,
173            light_config.standard_value,
174        )
175    };
176
177    if let Some(replacement) = replacement_light_data {
178        let use_global_fallbacks = replacement.color.is_none();
179        if let Some(fixed_color) = replacement.color {
180            light_as_hsv = color_to_hsv(fixed_color);
181        }
182
183        apply_hsv_replacement(
184            &mut light_as_hsv,
185            replacement,
186            global_hue,
187            global_saturation,
188            global_value,
189            use_global_fallbacks,
190        );
191
192        if let Some(duration_mult) = replacement.duration_mult {
193            light.data.time = scaled_i32(light.data.time, duration_mult);
194        } else if let Some(fixed_duration) = replacement.duration {
195            light.data.time = fixed_duration_to_i32(fixed_duration);
196        } else {
197            light.data.time = scaled_i32(light.data.time, light_config.duration_mult);
198        }
199
200        if let Some(radius_mult) = replacement.radius_mult {
201            light.data.radius = scaled_u32(light.data.radius, radius_mult);
202        } else if let Some(fixed_radius) = replacement.radius {
203            light.data.radius = fixed_radius;
204        } else {
205            light.data.radius = scaled_u32(light.data.radius, global_radius);
206        }
207
208        if let Some(flag) = &replacement.flag {
209            light.data.flags = flag.to_esp_flag();
210        }
211    } else {
212        apply_plain_hsv_adjustment(
213            &mut light_as_hsv,
214            global_hue,
215            global_saturation,
216            global_value,
217        );
218
219        light.data.radius = scaled_u32(light.data.radius, global_radius);
220        light.data.time = scaled_i32(light.data.time, light_config.duration_mult);
221    }
222
223    if let Some(replacement) = replacement_light_data {
224        let rgb8_color: Srgb<u8> = <Hsv as IntoColor<Srgb>>::into_color(light_as_hsv).into_format();
225        light.data.color = [rgb8_color.red, rgb8_color.green, rgb8_color.blue, 0];
226        apply_rgb_multipliers(&mut light.data.color, replacement);
227    } else {
228        let rgb8_color: Srgb<u8> = <Hsv as IntoColor<Srgb>>::into_color(light_as_hsv).into_format();
229        light.data.color = [rgb8_color.red, rgb8_color.green, rgb8_color.blue, 0];
230    }
231
232    light_changes(&original_data, &light.data)
233}
234
235fn light_changes(original: &tes3::esp::LightData, modified: &tes3::esp::LightData) -> Vec<String> {
236    let mut changes = Vec::new();
237
238    if original.color != modified.color {
239        changes.push(format!(
240            "color {:?} -> {:?}",
241            original.color, modified.color
242        ));
243    }
244
245    if original.radius != modified.radius {
246        changes.push(format!("radius {} -> {}", original.radius, modified.radius));
247    }
248
249    if original.time != modified.time {
250        changes.push(format!("duration {} -> {}", original.time, modified.time));
251    }
252
253    if original.flags != modified.flags {
254        changes.push(format!(
255            "flags {:?} -> {:?}",
256            original.flags, modified.flags
257        ));
258    }
259
260    changes
261}
262
263#[cfg(test)]
264mod tests {
265    use regex::Regex;
266    use tes3::esp::{Light, LightData, LightFlags, ObjectFlags};
267
268    use super::*;
269    use crate::light_override::LightFlag;
270
271    fn rgb_from_hsv(hue: f32, saturation: f32, value: f32) -> [u8; 4] {
272        let hsv = Hsv::from_components((palette::RgbHue::from_degrees(hue), saturation, value));
273        let rgb8_color: Srgb<u8> = <Hsv as IntoColor<Srgb>>::into_color(hsv).into_format();
274
275        [rgb8_color.red, rgb8_color.green, rgb8_color.blue, 0]
276    }
277
278    fn light(id: &str, hue: f32, radius: u32, time: i32, flags: LightFlags) -> Light {
279        Light {
280            flags: ObjectFlags::default(),
281            id: id.to_owned(),
282            data: LightData {
283                radius,
284                time,
285                color: rgb_from_hsv(hue, 1.0, 1.0),
286                flags,
287                ..LightData::default()
288            },
289            ..Light::default()
290        }
291    }
292
293    fn config() -> LightConfig {
294        LightConfig {
295            disable_flickering: false,
296            disable_pulse: false,
297            disable_negative_lights: true,
298            standard_hue: 1.0,
299            standard_saturation: 1.0,
300            standard_value: 1.0,
301            standard_radius: 1.0,
302            colored_hue: 1.0,
303            colored_saturation: 1.0,
304            colored_value: 1.0,
305            colored_radius: 1.0,
306            duration_mult: 1.0,
307            ..LightConfig::default()
308        }
309    }
310
311    const fn flag(flags: LightFlags) -> LightFlag {
312        LightFlag::from_esp_flags(flags)
313    }
314
315    #[test]
316    fn negative_lights_are_zeroed_and_return_early() {
317        let mut light_config = config();
318        light_config.disable_flickering = true;
319        light_config.disable_pulse = true;
320        light_config.standard_radius = 100.0;
321        light_config.duration_mult = 100.0;
322        light_config.light_regexes.push((
323            Regex::new("negative").unwrap(),
324            CustomLightData {
325                radius: Some(777),
326                duration: Some(888.0),
327                ..CustomLightData::default()
328            },
329        ));
330
331        let mut light = light(
332            "negative_light",
333            30.0,
334            42,
335            13,
336            LightFlags::NEGATIVE | LightFlags::FLICKER | LightFlags::PULSE,
337        );
338
339        process_light(&light_config, &mut light);
340
341        assert!(!light.data.flags.contains(LightFlags::NEGATIVE));
342        assert!(light.data.flags.contains(LightFlags::FLICKER));
343        assert!(light.data.flags.contains(LightFlags::PULSE));
344        assert_eq!(light.data.radius, 0);
345        assert_eq!(light.data.time, 13);
346        assert_eq!(light.data.color, [0, 0, 0, 0]);
347    }
348
349    #[test]
350    fn negative_lights_are_processed_normally_when_not_disabled() {
351        let mut light_config = config();
352        light_config.disable_negative_lights = false;
353        light_config.standard_radius = 2.0;
354        light_config.duration_mult = 3.0;
355
356        let mut light = light("negative_light", 30.0, 42, 13, LightFlags::NEGATIVE);
357
358        process_light(&light_config, &mut light);
359
360        assert!(light.data.flags.contains(LightFlags::NEGATIVE));
361        assert_eq!(light.data.radius, 84);
362        assert_eq!(light.data.time, 39);
363    }
364
365    #[test]
366    fn disabling_flicker_and_pulse_removes_only_those_flags() {
367        let mut light_config = config();
368        light_config.disable_flickering = true;
369        light_config.disable_pulse = true;
370
371        let mut light = light(
372            "animated_light",
373            30.0,
374            100,
375            10,
376            LightFlags::FLICKER
377                | LightFlags::FLICKER_SLOW
378                | LightFlags::PULSE
379                | LightFlags::PULSE_SLOW
380                | LightFlags::FIRE,
381        );
382
383        process_light(&light_config, &mut light);
384
385        assert!(!light.data.flags.contains(LightFlags::FLICKER));
386        assert!(!light.data.flags.contains(LightFlags::FLICKER_SLOW));
387        assert!(!light.data.flags.contains(LightFlags::PULSE));
388        assert!(!light.data.flags.contains(LightFlags::PULSE_SLOW));
389        assert!(light.data.flags.contains(LightFlags::FIRE));
390    }
391
392    #[test]
393    fn light_to_hsv_classifies_orange_boundaries_as_standard() {
394        for (hue, expected_colored) in [(13.0, true), (14.0, false), (64.0, false), (65.0, true)] {
395            let light = light("classified", hue, 1, 1, LightFlags::default());
396            let (_, is_colored) = light_to_hsv(&light.data);
397
398            assert_eq!(is_colored, expected_colored, "hue {hue}");
399        }
400    }
401
402    #[test]
403    fn standard_and_colored_lights_use_their_own_global_multipliers() {
404        let mut light_config = config();
405        light_config.standard_radius = 2.0;
406        light_config.colored_radius = 3.0;
407        light_config.duration_mult = 4.0;
408
409        let mut standard = light("standard", 30.0, 10, 5, LightFlags::default());
410        let mut colored = light("colored", 180.0, 10, 5, LightFlags::default());
411
412        process_light(&light_config, &mut standard);
413        process_light(&light_config, &mut colored);
414
415        assert_eq!(standard.data.radius, 20);
416        assert_eq!(colored.data.radius, 30);
417        assert_eq!(standard.data.time, 20);
418        assert_eq!(colored.data.time, 20);
419    }
420
421    #[test]
422    fn matching_light_overrides_beat_globals_and_fall_back_per_field() {
423        let mut light_config = config();
424        light_config.standard_radius = 2.0;
425        light_config.duration_mult = 3.0;
426        light_config.light_regexes.push((
427            Regex::new("fixed").unwrap(),
428            CustomLightData {
429                radius: Some(123),
430                duration: Some(456.0),
431                color: Some([0, 128, 64, 0]),
432                ..CustomLightData::default()
433            },
434        ));
435        light_config.light_regexes.push((
436            Regex::new("partial").unwrap(),
437            CustomLightData {
438                radius: Some(321),
439                ..CustomLightData::default()
440            },
441        ));
442        light_config.light_regexes.push((
443            Regex::new("mult").unwrap(),
444            CustomLightData {
445                radius_mult: Some(5.0),
446                duration_mult: Some(7.0),
447                ..CustomLightData::default()
448            },
449        ));
450
451        let mut fixed = light("fixed_light", 30.0, 10, 10, LightFlags::default());
452        let mut partial = light("partial_light", 30.0, 10, 10, LightFlags::default());
453        let mut mult = light("mult_light", 30.0, 10, 10, LightFlags::default());
454
455        process_light(&light_config, &mut fixed);
456        process_light(&light_config, &mut partial);
457        process_light(&light_config, &mut mult);
458
459        assert_eq!(fixed.data.radius, 123);
460        assert_eq!(fixed.data.time, 456);
461        assert_eq!(fixed.data.color, [0, 128, 64, 0]);
462
463        assert_eq!(partial.data.radius, 321);
464        assert_eq!(partial.data.time, 30);
465
466        assert_eq!(mult.data.radius, 50);
467        assert_eq!(mult.data.time, 70);
468    }
469
470    #[test]
471    fn partial_legacy_hsv_overrides_are_still_applied_at_runtime() {
472        let mut light_config = config();
473        light_config.standard_hue = 1.0;
474        light_config.standard_saturation = 1.0;
475        light_config.standard_value = 1.0;
476        light_config.light_regexes.push((
477            Regex::new("legacy_partial").unwrap(),
478            CustomLightData {
479                hue: Some(180),
480                saturation: Some(0.5),
481                ..CustomLightData::default()
482            },
483        ));
484        let mut light = light("legacy_partial", 30.0, 10, 10, LightFlags::default());
485
486        process_light(&light_config, &mut light);
487
488        assert_eq!(light.data.color, rgb_from_hsv(180.0, 0.5, 1.0));
489    }
490
491    #[test]
492    fn first_matching_light_override_wins_and_flag_replacement_is_exact() {
493        let mut light_config = config();
494        light_config.standard_radius = 10.0;
495        light_config.light_regexes.push((
496            Regex::new("torch").unwrap(),
497            CustomLightData {
498                radius: Some(111),
499                flag: Some(flag(LightFlags::PULSE_SLOW)),
500                ..CustomLightData::default()
501            },
502        ));
503        light_config.light_regexes.push((
504            Regex::new("torch_special").unwrap(),
505            CustomLightData {
506                radius: Some(222),
507                flag: Some(flag(LightFlags::FLICKER)),
508                ..CustomLightData::default()
509            },
510        ));
511        let mut light = light(
512            "torch_special",
513            30.0,
514            10,
515            10,
516            LightFlags::CAN_CARRY | LightFlags::FIRE | LightFlags::FLICKER,
517        );
518
519        process_light(&light_config, &mut light);
520
521        assert_eq!(light.data.radius, 111);
522        assert_eq!(light.data.flags, LightFlags::PULSE_SLOW);
523    }
524
525    #[test]
526    fn none_flag_clears_all_flags() {
527        let mut light_config = config();
528        light_config.light_regexes.push((
529            Regex::new("torch").unwrap(),
530            CustomLightData {
531                flag: Some(flag(LightFlags::empty())),
532                ..CustomLightData::default()
533            },
534        ));
535        let mut light = light(
536            "torch",
537            30.0,
538            10,
539            10,
540            LightFlags::CAN_CARRY | LightFlags::FIRE | LightFlags::FLICKER | LightFlags::PULSE_SLOW,
541        );
542
543        process_light(&light_config, &mut light);
544
545        assert_eq!(light.data.flags, LightFlags::empty());
546    }
547
548    #[test]
549    fn flag_replacement_strips_source_bits_not_named_by_user() {
550        let mut light_config = config();
551        light_config.light_regexes.push((
552            Regex::new("torch").unwrap(),
553            CustomLightData {
554                flag: Some(flag(LightFlags::PULSE)),
555                ..CustomLightData::default()
556            },
557        ));
558        let unknown_flag = LightFlags::from_bits_retain(0x200);
559        let mut light = light(
560            "torch",
561            30.0,
562            10,
563            10,
564            unknown_flag | LightFlags::CAN_CARRY | LightFlags::FLICKER,
565        );
566
567        process_light(&light_config, &mut light);
568
569        assert_eq!(light.data.flags, LightFlags::PULSE);
570    }
571
572    #[test]
573    fn flag_override_can_insert_carryable_flags() {
574        let mut light_config = config();
575        light_config.light_regexes.push((
576            Regex::new("torch").unwrap(),
577            CustomLightData {
578                flag: Some(flag(LightFlags::CAN_CARRY | LightFlags::PULSE_SLOW)),
579                ..CustomLightData::default()
580            },
581        ));
582        let mut light = light(
583            "torch",
584            30.0,
585            10,
586            10,
587            LightFlags::FIRE | LightFlags::FLICKER,
588        );
589
590        process_light(&light_config, &mut light);
591
592        assert_eq!(
593            light.data.flags,
594            LightFlags::CAN_CARRY | LightFlags::PULSE_SLOW
595        );
596    }
597
598    #[test]
599    fn hsv_multiplier_overrides_apply_to_matching_lights() {
600        let mut light_config = config();
601        light_config.light_regexes.push((
602            Regex::new("hsv_mult").unwrap(),
603            CustomLightData {
604                hue_mult: Some(2.0),
605                saturation_mult: Some(0.5),
606                value_mult: Some(0.25),
607                ..CustomLightData::default()
608            },
609        ));
610        let mut light = light("hsv_mult_light", 30.0, 10, 10, LightFlags::default());
611
612        process_light(&light_config, &mut light);
613
614        assert_eq!(light.data.color, rgb_from_hsv(60.0, 0.5, 0.25));
615    }
616
617    #[test]
618    fn rgb_multipliers_apply_after_hsv_adjustments() {
619        let mut light_config = config();
620        light_config.light_regexes.push((
621            Regex::new("rgb_after_hsv").unwrap(),
622            CustomLightData {
623                hue_mult: Some(2.0),
624                saturation_mult: Some(0.5),
625                value_mult: Some(0.25),
626                red_mult: Some(0.5),
627                green_mult: Some(2.0),
628                blue_mult: Some(-1.0),
629                ..CustomLightData::default()
630            },
631        ));
632        let mut light = light("rgb_after_hsv_light", 30.0, 10, 10, LightFlags::default());
633
634        process_light(&light_config, &mut light);
635
636        let mut expected = rgb_from_hsv(60.0, 0.5, 0.25);
637        expected[0] = scaled_u8(expected[0], 0.5);
638        expected[1] = scaled_u8(expected[1], 2.0);
639        expected[2] = 0;
640        assert_eq!(light.data.color, expected);
641    }
642
643    #[test]
644    fn fixed_rgb_gets_rgb_multipliers() {
645        let mut light_config = config();
646        light_config.standard_hue = 10.0;
647        light_config.standard_saturation = 0.0;
648        light_config.standard_value = 0.0;
649        light_config.light_regexes.push((
650            Regex::new("fixed_rgb").unwrap(),
651            CustomLightData {
652                color: Some([100, 80, 60, 0]),
653                red_mult: Some(3.0),
654                green_mult: Some(0.5),
655                blue_mult: Some(1.0),
656                ..CustomLightData::default()
657            },
658        ));
659        let mut light = light("fixed_rgb_light", 30.0, 10, 10, LightFlags::default());
660
661        process_light(&light_config, &mut light);
662
663        assert_eq!(light.data.color, [255, 40, 60, 0]);
664    }
665
666    #[test]
667    fn fixed_rgb_is_base_color_for_hsv_adjustments() {
668        let mut light_config = config();
669        light_config.standard_hue = 10.0;
670        light_config.standard_saturation = 0.0;
671        light_config.standard_value = 0.0;
672        light_config.light_regexes.push((
673            Regex::new("fixed_rgb_hsv").unwrap(),
674            CustomLightData {
675                color: Some([255, 0, 0, 0]),
676                hue: Some(120),
677                green_mult: Some(0.5),
678                ..CustomLightData::default()
679            },
680        ));
681        let mut light = light("fixed_rgb_hsv_light", 30.0, 10, 10, LightFlags::default());
682
683        process_light(&light_config, &mut light);
684
685        assert_eq!(light.data.color, [0, 127, 0, 0]);
686    }
687
688    #[test]
689    fn negative_radius_multipliers_clamp_to_zero_instead_of_wrapping() {
690        let mut light_config = config();
691        light_config.standard_radius = -2.0;
692        light_config.light_regexes.push((
693            Regex::new("override").unwrap(),
694            CustomLightData {
695                radius_mult: Some(-3.0),
696                ..CustomLightData::default()
697            },
698        ));
699        let mut global = light("global", 30.0, 10, 10, LightFlags::default());
700        let mut overridden = light("override", 30.0, 10, 10, LightFlags::default());
701
702        process_light(&light_config, &mut global);
703        process_light(&light_config, &mut overridden);
704
705        assert_eq!(global.data.radius, 0);
706        assert_eq!(overridden.data.radius, 0);
707    }
708}