Skip to main content

s3lightfixes/
light_override.rs

1use std::{fmt, str::FromStr};
2
3use palette::{Hsv, IntoColor, rgb::Srgb};
4use serde::{Deserialize, Serialize, ser::SerializeStruct};
5
6#[derive(Debug)]
7pub enum ParseLightError {
8    ExclusiveFields(&'static str, &'static str),
9    IncompleteRgb,
10    BadPair(String),
11    UnknownField(String),
12    BadNumber(&'static str, String),
13    MissingPrefix,
14    UnknownVariant(String),
15}
16
17impl std::fmt::Display for ParseLightError {
18    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
19        use ParseLightError::{
20            BadNumber, BadPair, ExclusiveFields, IncompleteRgb, MissingPrefix, UnknownField,
21            UnknownVariant,
22        };
23        match self {
24            BadPair(s) => write!(f, "Expected key=value pair, got: `{s}`"),
25            ExclusiveFields(existing_field, bad_field) => write!(
26                f,
27                "Key {existing_field} is mutually exclusive with {bad_field}"
28            ),
29            IncompleteRgb => write!(f, "RGB overrides must specify red, green, and blue"),
30            UnknownField(k) => write!(f, "Unknown field: `{k}`"),
31            BadNumber(field, e) => write!(f, "Invalid number for `{field}`: {e}"),
32            MissingPrefix => write!(f, "Missing type prefix (e.g., `Fixed:` or `Mult:`)"),
33            UnknownVariant(v) => {
34                write!(f, "Unknown light type: `{v}` (expected `Fixed` or `Mult`)")
35            }
36        }
37    }
38}
39
40impl std::error::Error for ParseLightError {}
41
42fn parse_pairs<F>(s: &str, mut set: F) -> Result<(), ParseLightError>
43where
44    F: FnMut(&str, &str) -> Result<(), ParseLightError>,
45{
46    for pair in s.split(',').filter(|p| !p.trim().is_empty()) {
47        let (k, v) = pair
48            .split_once('=')
49            .ok_or_else(|| ParseLightError::BadPair(pair.to_string()))?;
50        set(k.trim(), v.trim())?;
51    }
52    Ok(())
53}
54
55impl FromStr for CustomLightData {
56    type Err = ParseLightError;
57
58    fn from_str(s: &str) -> Result<Self, Self::Err> {
59        let mut data = CustomLightData::default();
60        let mut color = RgbBuilder::default();
61        parse_pairs(s, |key, value| data.set_pair(key, value, &mut color))?;
62        data.color = color.finish()?;
63
64        Ok(data)
65    }
66}
67
68pub fn parse_light_override(s: &str) -> Result<(String, CustomLightData), ParseLightError> {
69    let (id, setting) = s
70        .split_once('=')
71        .ok_or_else(|| ParseLightError::BadPair(s.to_string()))?;
72
73    let parsed_setting: CustomLightData = setting.parse()?;
74    Ok((id.to_string(), parsed_setting))
75}
76
77pub fn parse_ambient_override(s: &str) -> Result<(String, CustomCellAmbient), ParseAmbientError> {
78    let (id, setting) = s
79        .split_once('=')
80        .ok_or_else(|| ParseAmbientError::BadPair(s.to_string()))?;
81
82    let parsed_setting: CustomCellAmbient = setting.parse()?;
83    Ok((id.to_string(), parsed_setting))
84}
85
86#[derive(Deserialize)]
87struct RawCustomLightData {
88    red: Option<u8>,
89    green: Option<u8>,
90    blue: Option<u8>,
91    red_mult: Option<f32>,
92    green_mult: Option<f32>,
93    blue_mult: Option<f32>,
94    hue: Option<u32>,
95    saturation: Option<f32>,
96    value: Option<f32>,
97    hue_mult: Option<f32>,
98    saturation_mult: Option<f32>,
99    value_mult: Option<f32>,
100    radius: Option<u32>,
101    radius_mult: Option<f32>,
102    duration: Option<f32>,
103    duration_mult: Option<f32>,
104    flag: Option<LightFlag>,
105}
106
107impl<'de> serde::Deserialize<'de> for CustomLightData {
108    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
109    where
110        D: serde::Deserializer<'de>,
111    {
112        let raw = RawCustomLightData::deserialize(deserializer)?;
113
114        // Check exclusivity
115        macro_rules! check_exclusive {
116            ($field:ident, $mult:ident) => {
117                if raw.$field.is_some() && raw.$mult.is_some() {
118                    return Err(serde::de::Error::custom(format!(
119                        "Fields `{}` and `{}` are mutually exclusive",
120                        stringify!($field),
121                        stringify!($mult)
122                    )));
123                }
124            };
125        }
126
127        check_exclusive!(radius, radius_mult);
128        check_exclusive!(duration, duration_mult);
129
130        let red_mult = finite_float(raw.red_mult, "red_mult").map_err(serde::de::Error::custom)?;
131        let green_mult =
132            finite_float(raw.green_mult, "green_mult").map_err(serde::de::Error::custom)?;
133        let blue_mult =
134            finite_float(raw.blue_mult, "blue_mult").map_err(serde::de::Error::custom)?;
135        let hue_mult = finite_float(raw.hue_mult, "hue_mult").map_err(serde::de::Error::custom)?;
136        let saturation_mult = finite_float(raw.saturation_mult, "saturation_mult")
137            .map_err(serde::de::Error::custom)?;
138        let value_mult =
139            finite_float(raw.value_mult, "value_mult").map_err(serde::de::Error::custom)?;
140        let radius_mult =
141            finite_float(raw.radius_mult, "radius_mult").map_err(serde::de::Error::custom)?;
142        let duration = finite_float(raw.duration, "duration").map_err(serde::de::Error::custom)?;
143        let duration_mult =
144            finite_float(raw.duration_mult, "duration_mult").map_err(serde::de::Error::custom)?;
145        let saturation =
146            finite_float(raw.saturation, "saturation").map_err(serde::de::Error::custom)?;
147        let value = finite_float(raw.value, "value").map_err(serde::de::Error::custom)?;
148
149        let rgb_color =
150            rgb_from_parts(raw.red, raw.green, raw.blue).map_err(serde::de::Error::custom)?;
151        let (hue, saturation, value) = keep_legacy_hsv(raw.hue, saturation, value);
152
153        if hue.is_some() && hue_mult.is_some() {
154            return Err(serde::de::Error::custom(
155                "Fields `hue` and `hue_mult` are mutually exclusive",
156            ));
157        }
158        if saturation.is_some() && saturation_mult.is_some() {
159            return Err(serde::de::Error::custom(
160                "Fields `saturation` and `saturation_mult` are mutually exclusive",
161            ));
162        }
163        if value.is_some() && value_mult.is_some() {
164            return Err(serde::de::Error::custom(
165                "Fields `value` and `value_mult` are mutually exclusive",
166            ));
167        }
168
169        Ok(CustomLightData {
170            color: rgb_color,
171            red_mult,
172            green_mult,
173            blue_mult,
174            hue,
175            saturation,
176            value,
177            hue_mult,
178            saturation_mult,
179            value_mult,
180            radius: raw.radius,
181            radius_mult,
182            duration,
183            duration_mult,
184            flag: raw.flag,
185        })
186    }
187}
188
189#[derive(Clone, Debug, Default)]
190pub struct CustomLightData {
191    pub color: Option<[u8; 4]>,
192    pub red_mult: Option<f32>,
193    pub green_mult: Option<f32>,
194    pub blue_mult: Option<f32>,
195    pub hue: Option<u32>,
196    pub saturation: Option<f32>,
197    pub value: Option<f32>,
198    pub hue_mult: Option<f32>,
199    pub saturation_mult: Option<f32>,
200    pub value_mult: Option<f32>,
201    pub radius: Option<u32>,
202    pub radius_mult: Option<f32>,
203    pub duration: Option<f32>,
204    pub duration_mult: Option<f32>,
205    pub flag: Option<LightFlag>,
206}
207
208impl Serialize for CustomLightData {
209    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
210    where
211        S: serde::Serializer,
212    {
213        let mut fields = 0;
214        fields += usize::from(self.color.is_some()) * 3;
215        fields += usize::from(self.red_mult.is_some());
216        fields += usize::from(self.green_mult.is_some());
217        fields += usize::from(self.blue_mult.is_some());
218        fields += usize::from(self.hue.is_some());
219        fields += usize::from(self.saturation.is_some());
220        fields += usize::from(self.value.is_some());
221        fields += usize::from(self.hue_mult.is_some());
222        fields += usize::from(self.saturation_mult.is_some());
223        fields += usize::from(self.value_mult.is_some());
224        fields += usize::from(self.radius.is_some());
225        fields += usize::from(self.radius_mult.is_some());
226        fields += usize::from(self.duration.is_some());
227        fields += usize::from(self.duration_mult.is_some());
228        fields += usize::from(self.flag.is_some());
229
230        let mut state = serializer.serialize_struct("CustomLightData", fields)?;
231        if let Some([red, green, blue, _]) = self.color {
232            state.serialize_field("red", &red)?;
233            state.serialize_field("green", &green)?;
234            state.serialize_field("blue", &blue)?;
235        }
236        if let Some(value) = self.red_mult {
237            state.serialize_field("red_mult", &value)?;
238        }
239        if let Some(value) = self.green_mult {
240            state.serialize_field("green_mult", &value)?;
241        }
242        if let Some(value) = self.blue_mult {
243            state.serialize_field("blue_mult", &value)?;
244        }
245        if let Some(value) = self.hue {
246            state.serialize_field("hue", &value)?;
247        }
248        if let Some(value) = self.saturation {
249            state.serialize_field("saturation", &value)?;
250        }
251        if let Some(value) = self.value {
252            state.serialize_field("value", &value)?;
253        }
254        if let Some(value) = self.hue_mult {
255            state.serialize_field("hue_mult", &value)?;
256        }
257        if let Some(value) = self.saturation_mult {
258            state.serialize_field("saturation_mult", &value)?;
259        }
260        if let Some(value) = self.value_mult {
261            state.serialize_field("value_mult", &value)?;
262        }
263        if let Some(value) = self.radius {
264            state.serialize_field("radius", &value)?;
265        }
266        if let Some(value) = self.radius_mult {
267            state.serialize_field("radius_mult", &value)?;
268        }
269        if let Some(value) = self.duration {
270            state.serialize_field("duration", &value)?;
271        }
272        if let Some(value) = self.duration_mult {
273            state.serialize_field("duration_mult", &value)?;
274        }
275        if let Some(value) = &self.flag {
276            state.serialize_field("flag", value)?;
277        }
278
279        state.end()
280    }
281}
282
283#[derive(Default)]
284struct RgbBuilder {
285    red: Option<u8>,
286    green: Option<u8>,
287    blue: Option<u8>,
288}
289
290impl RgbBuilder {
291    fn finish(self) -> Result<Option<[u8; 4]>, ParseLightError> {
292        rgb_from_parts(self.red, self.green, self.blue).map_err(|_| ParseLightError::IncompleteRgb)
293    }
294}
295
296fn rgb_from_parts(
297    red: Option<u8>,
298    green: Option<u8>,
299    blue: Option<u8>,
300) -> Result<Option<[u8; 4]>, &'static str> {
301    match (red, green, blue) {
302        (None, None, None) => Ok(None),
303        (Some(red), Some(green), Some(blue)) => Ok(Some([red, green, blue, 0])),
304        _ => Err("RGB overrides must specify red, green, and blue"),
305    }
306}
307
308fn finite_float(value: Option<f32>, field: &'static str) -> Result<Option<f32>, &'static str> {
309    match value {
310        Some(value) if value.is_finite() => Ok(Some(value)),
311        Some(_) => Err(match field {
312            "red_mult" => "red_mult must be finite",
313            "green_mult" => "green_mult must be finite",
314            "blue_mult" => "blue_mult must be finite",
315            "hue_mult" => "hue_mult must be finite",
316            "saturation_mult" => "saturation_mult must be finite",
317            "value_mult" => "value_mult must be finite",
318            "radius_mult" => "radius_mult must be finite",
319            "duration" => "duration must be finite",
320            "duration_mult" => "duration_mult must be finite",
321            "saturation" => "saturation must be finite",
322            "value" => "value must be finite",
323            _ => "float value must be finite",
324        }),
325        None => Ok(None),
326    }
327}
328
329fn legacy_hsv_to_rgb(
330    hue: Option<u32>,
331    saturation: Option<f32>,
332    value: Option<f32>,
333) -> Result<Option<[u8; 4]>, &'static str> {
334    match (hue, saturation, value) {
335        (None, None, None) => Ok(None),
336        (Some(hue), Some(saturation), Some(value)) => Ok(Some(hsv_to_rgb8(hue, saturation, value))),
337        _ => Err("HSV ambient colors must specify hue, saturation, and value"),
338    }
339}
340
341fn keep_legacy_hsv(
342    hue: Option<u32>,
343    saturation: Option<f32>,
344    value: Option<f32>,
345) -> (Option<u32>, Option<f32>, Option<f32>) {
346    (
347        hue.map(|hue| hue.clamp(0, 360)),
348        saturation.map(|saturation| saturation.clamp(0.0, 1.0)),
349        value.map(|value| value.clamp(0.0, 1.0)),
350    )
351}
352
353#[allow(clippy::cast_precision_loss)]
354fn hsv_to_rgb8(hue: u32, saturation: f32, value: f32) -> [u8; 4] {
355    let hsv = Hsv::from_components((
356        palette::RgbHue::from_degrees(hue.clamp(0, 360) as f32),
357        saturation.clamp(0.0, 1.0),
358        value.clamp(0.0, 1.0),
359    ));
360    let rgb8_color: Srgb<u8> = <Hsv as IntoColor<Srgb>>::into_color(hsv).into_format();
361
362    [rgb8_color.red, rgb8_color.green, rgb8_color.blue, 0]
363}
364
365fn parse_clamped_unit_float(field: &'static str, value: &str) -> Result<f32, ParseLightError> {
366    let value = value
367        .parse::<f32>()
368        .map_err(|e| ParseLightError::BadNumber(field, e.to_string()))?;
369    if !value.is_finite() {
370        return Err(ParseLightError::BadNumber(
371            field,
372            "value must be finite".to_owned(),
373        ));
374    }
375    Ok(value.clamp(0.0, 1.0))
376}
377
378impl CustomLightData {
379    fn set_float_mult(
380        target: &mut Option<f32>,
381        fixed_is_set: bool,
382        fixed_name: &'static str,
383        mult_name: &'static str,
384        value: &str,
385    ) -> Result<(), ParseLightError> {
386        if fixed_is_set {
387            return Err(ParseLightError::ExclusiveFields(fixed_name, mult_name));
388        }
389
390        *target = Some(value.parse().map_err(|e: std::num::ParseFloatError| {
391            ParseLightError::BadNumber(mult_name, e.to_string())
392        })?);
393        if target.as_ref().is_some_and(|value| !value.is_finite()) {
394            return Err(ParseLightError::BadNumber(
395                mult_name,
396                "value must be finite".to_owned(),
397            ));
398        }
399        Ok(())
400    }
401
402    fn set_pair(
403        &mut self,
404        key: &str,
405        value: &str,
406        color: &mut RgbBuilder,
407    ) -> Result<(), ParseLightError> {
408        match key {
409            "radius_mult" => Self::set_float_mult(
410                &mut self.radius_mult,
411                self.radius.is_some(),
412                "radius",
413                "radius_mult",
414                value,
415            ),
416            "red_mult" => Self::set_plain_float(&mut self.red_mult, "red_mult", value),
417            "green_mult" => Self::set_plain_float(&mut self.green_mult, "green_mult", value),
418            "blue_mult" => Self::set_plain_float(&mut self.blue_mult, "blue_mult", value),
419            "hue_mult" => Self::set_float_mult(
420                &mut self.hue_mult,
421                self.hue.is_some(),
422                "hue",
423                "hue_mult",
424                value,
425            ),
426            "saturation_mult" => Self::set_float_mult(
427                &mut self.saturation_mult,
428                self.saturation.is_some(),
429                "saturation",
430                "saturation_mult",
431                value,
432            ),
433            "value_mult" => Self::set_float_mult(
434                &mut self.value_mult,
435                self.value.is_some(),
436                "value",
437                "value_mult",
438                value,
439            ),
440            "duration_mult" => Self::set_float_mult(
441                &mut self.duration_mult,
442                self.duration.is_some(),
443                "duration",
444                "duration_mult",
445                value,
446            ),
447            "duration" => self.set_duration(value),
448            "radius" => self.set_radius(value),
449            "hue" => self.set_hue(value),
450            "saturation" => self.set_saturation(value),
451            "value" => self.set_value(value),
452            "red" => Self::set_color_component(&mut color.red, "red", value),
453            "green" => Self::set_color_component(&mut color.green, "green", value),
454            "blue" => Self::set_color_component(&mut color.blue, "blue", value),
455            "flag" => {
456                self.flag = Some(value.parse()?);
457                Ok(())
458            }
459            _ => Err(ParseLightError::UnknownField(key.to_owned())),
460        }
461    }
462
463    fn set_duration(&mut self, value: &str) -> Result<(), ParseLightError> {
464        if self.duration_mult.is_some() {
465            return Err(ParseLightError::ExclusiveFields(
466                "duration_mult",
467                "duration",
468            ));
469        }
470        self.duration = Some(value.parse().map_err(|e: std::num::ParseFloatError| {
471            ParseLightError::BadNumber("duration", e.to_string())
472        })?);
473        if self.duration.is_some_and(|value| !value.is_finite()) {
474            return Err(ParseLightError::BadNumber(
475                "duration",
476                "value must be finite".to_owned(),
477            ));
478        }
479        Ok(())
480    }
481
482    fn set_plain_float(
483        target: &mut Option<f32>,
484        field: &'static str,
485        value: &str,
486    ) -> Result<(), ParseLightError> {
487        *target = Some(value.parse().map_err(|e: std::num::ParseFloatError| {
488            ParseLightError::BadNumber(field, e.to_string())
489        })?);
490        if target.as_ref().is_some_and(|value| !value.is_finite()) {
491            return Err(ParseLightError::BadNumber(
492                field,
493                "value must be finite".to_owned(),
494            ));
495        }
496        Ok(())
497    }
498
499    fn set_hue(&mut self, value: &str) -> Result<(), ParseLightError> {
500        if self.hue_mult.is_some() {
501            return Err(ParseLightError::ExclusiveFields("hue_mult", "hue"));
502        }
503        self.hue = Some(
504            value
505                .parse::<u32>()
506                .map_err(|e| ParseLightError::BadNumber("hue", e.to_string()))?
507                .clamp(0, 360),
508        );
509        Ok(())
510    }
511
512    fn set_saturation(&mut self, value: &str) -> Result<(), ParseLightError> {
513        if self.saturation_mult.is_some() {
514            return Err(ParseLightError::ExclusiveFields(
515                "saturation_mult",
516                "saturation",
517            ));
518        }
519        self.saturation = Some(parse_clamped_unit_float("saturation", value)?);
520        Ok(())
521    }
522
523    fn set_value(&mut self, value: &str) -> Result<(), ParseLightError> {
524        if self.value_mult.is_some() {
525            return Err(ParseLightError::ExclusiveFields("value_mult", "value"));
526        }
527        self.value = Some(parse_clamped_unit_float("value", value)?);
528        Ok(())
529    }
530
531    fn set_radius(&mut self, value: &str) -> Result<(), ParseLightError> {
532        if self.radius_mult.is_some() {
533            return Err(ParseLightError::ExclusiveFields("radius_mult", "radius"));
534        }
535        self.radius = Some(value.parse().map_err(|e: std::num::ParseIntError| {
536            ParseLightError::BadNumber("radius", e.to_string())
537        })?);
538        Ok(())
539    }
540
541    fn set_color_component(
542        target: &mut Option<u8>,
543        field: &'static str,
544        value: &str,
545    ) -> Result<(), ParseLightError> {
546        *target = Some(value.parse().map_err(|e: std::num::ParseIntError| {
547            ParseLightError::BadNumber(field, e.to_string())
548        })?);
549        Ok(())
550    }
551}
552
553#[derive(Clone, Debug, Default, Serialize)]
554/// RGB color replacement using the same component range as TES3 light records.
555pub struct TypedLightColor {
556    pub red: u8,
557    pub green: u8,
558    pub blue: u8,
559}
560
561#[derive(Deserialize)]
562struct RawTypedLightColor {
563    red: Option<u8>,
564    green: Option<u8>,
565    blue: Option<u8>,
566    hue: Option<u32>,
567    saturation: Option<f32>,
568    value: Option<f32>,
569}
570
571impl<'de> Deserialize<'de> for TypedLightColor {
572    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
573    where
574        D: serde::Deserializer<'de>,
575    {
576        let raw = RawTypedLightColor::deserialize(deserializer)?;
577        let rgb_color =
578            rgb_from_parts(raw.red, raw.green, raw.blue).map_err(serde::de::Error::custom)?;
579        let legacy_hsv_color = legacy_hsv_to_rgb(raw.hue, raw.saturation, raw.value)
580            .map_err(serde::de::Error::custom)?;
581
582        if rgb_color.is_some() && legacy_hsv_color.is_some() {
583            return Err(serde::de::Error::custom(
584                "RGB color fields are mutually exclusive with legacy HSV color fields",
585            ));
586        }
587
588        let Some([red, green, blue, _]) = rgb_color.or(legacy_hsv_color) else {
589            return Err(serde::de::Error::custom(
590                "RGB colors must specify red, green, and blue",
591            ));
592        };
593
594        Ok(Self { red, green, blue })
595    }
596}
597
598impl TypedLightColor {
599    pub const fn to_esp_color(&self) -> [u8; 4] {
600        [self.red, self.green, self.blue, 0]
601    }
602}
603
604#[derive(Debug)]
605pub enum ParseTypedColorError {
606    MissingField(&'static str),
607    UnknownField(String),
608    BadNumber(&'static str, String),
609    BadPair(String),
610}
611
612impl fmt::Display for ParseTypedColorError {
613    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
614        use ParseTypedColorError::{BadNumber, BadPair, MissingField, UnknownField};
615        match self {
616            MissingField(name) => write!(f, "Missing required field: `{name}`"),
617            UnknownField(name) => write!(f, "Unknown field: `{name}`"),
618            BadNumber(field, msg) => write!(f, "Invalid value for `{field}`: {msg}"),
619            BadPair(pair) => write!(f, "Expected key=value pair, got: `{pair}`"),
620        }
621    }
622}
623
624impl std::error::Error for ParseTypedColorError {}
625
626impl FromStr for TypedLightColor {
627    type Err = ParseTypedColorError;
628
629    fn from_str(s: &str) -> Result<Self, Self::Err> {
630        let mut red: Option<u8> = None;
631        let mut green: Option<u8> = None;
632        let mut blue: Option<u8> = None;
633
634        for pair in s.split(',').filter(|p| !p.trim().is_empty()) {
635            let (k, v) = pair
636                .split_once('=')
637                .ok_or_else(|| ParseTypedColorError::BadPair(pair.to_string()))?;
638
639            match k.trim() {
640                "red" => {
641                    red = Some(v.trim().parse().map_err(|e: std::num::ParseIntError| {
642                        ParseTypedColorError::BadNumber("red", e.to_string())
643                    })?);
644                }
645                "green" => {
646                    green = Some(v.trim().parse().map_err(|e: std::num::ParseIntError| {
647                        ParseTypedColorError::BadNumber("green", e.to_string())
648                    })?);
649                }
650                "blue" => {
651                    blue = Some(v.trim().parse().map_err(|e: std::num::ParseIntError| {
652                        ParseTypedColorError::BadNumber("blue", e.to_string())
653                    })?);
654                }
655                other => return Err(ParseTypedColorError::UnknownField(other.to_string())),
656            }
657        }
658
659        Ok(TypedLightColor {
660            red: red.ok_or(ParseTypedColorError::MissingField("red"))?,
661            green: green.ok_or(ParseTypedColorError::MissingField("green"))?,
662            blue: blue.ok_or(ParseTypedColorError::MissingField("blue"))?,
663        })
664    }
665}
666
667#[derive(Clone, Debug, Default, Deserialize, Serialize)]
668pub struct CustomCellAmbient {
669    pub ambient: Option<TypedLightColor>,
670    pub sunlight: Option<TypedLightColor>,
671    pub fog: Option<TypedLightColor>,
672    pub fog_density: Option<f32>,
673}
674
675#[derive(Debug)]
676pub enum ParseAmbientError {
677    BadPair(String),
678    UnknownField(String),
679    BadColor(String, Box<dyn std::error::Error + Send + Sync>),
680}
681
682impl fmt::Display for ParseAmbientError {
683    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
684        use ParseAmbientError::{BadColor, BadPair, UnknownField};
685        match self {
686            BadPair(pair) => write!(f, "Expected key=value pair, got: `{pair}`"),
687            UnknownField(field) => write!(f, "Unknown field: `{field}`"),
688            BadColor(field, err) => write!(f, "Invalid color for `{field}`: {err}"),
689        }
690    }
691}
692
693impl std::error::Error for ParseAmbientError {}
694
695impl FromStr for CustomCellAmbient {
696    type Err = ParseAmbientError;
697
698    fn from_str(s: &str) -> Result<Self, Self::Err> {
699        let mut ambient = None;
700        let mut sunlight = None;
701        let mut fog = None;
702        let mut fog_density = None;
703
704        for pair in s.split(';').filter(|p| !p.trim().is_empty()) {
705            let (key, value) = pair
706                .split_once('=')
707                .ok_or_else(|| ParseAmbientError::BadPair(pair.to_string()))?;
708
709            match key.trim() {
710                "ambient" => {
711                    let parsed = value
712                        .parse()
713                        .map_err(|e| ParseAmbientError::BadColor("ambient".into(), Box::new(e)))?;
714                    ambient = Some(parsed);
715                }
716                "sunlight" => {
717                    let parsed = value
718                        .parse()
719                        .map_err(|e| ParseAmbientError::BadColor("sunlight".into(), Box::new(e)))?;
720                    sunlight = Some(parsed);
721                }
722                "fog" => {
723                    let parsed = value
724                        .parse()
725                        .map_err(|e| ParseAmbientError::BadColor("fog".into(), Box::new(e)))?;
726                    fog = Some(parsed);
727                }
728                "fog_density" => {
729                    let parsed: f32 = value.parse().map_err(|e| {
730                        ParseAmbientError::BadColor("fog_density".into(), Box::new(e))
731                    })?;
732                    fog_density = Some(parsed);
733                }
734                other => return Err(ParseAmbientError::UnknownField(other.to_string())),
735            }
736        }
737
738        Ok(CustomCellAmbient {
739            ambient,
740            sunlight,
741            fog,
742            fog_density,
743        })
744    }
745}
746
747#[derive(Clone, Debug, Default, Deserialize, Serialize)]
748pub enum LightFlag {
749    #[serde(rename = "FLICKERSLOW")]
750    FlickerSlow,
751    #[serde(rename = "FLICKER")]
752    Flicker,
753    #[serde(rename = "PULSE")]
754    Pulse,
755    #[serde(rename = "PULSESLOW")]
756    PulseSlow,
757    #[default]
758    #[serde(rename = "NONE")]
759    None,
760}
761
762use tes3::esp::LightFlags;
763impl LightFlag {
764    pub fn to_esp_flag(&self) -> LightFlags {
765        match &self {
766            Self::Flicker => LightFlags::FLICKER,
767            Self::FlickerSlow => LightFlags::FLICKER_SLOW,
768            Self::Pulse => LightFlags::PULSE,
769            Self::PulseSlow => LightFlags::PULSE_SLOW,
770            Self::None => LightFlags::empty(),
771        }
772    }
773}
774
775impl FromStr for LightFlag {
776    type Err = ParseLightError;
777
778    fn from_str(s: &str) -> Result<Self, Self::Err> {
779        match s.to_ascii_lowercase().as_str() {
780            "flicker" => Ok(LightFlag::Flicker),
781            "flickerslow" => Ok(LightFlag::FlickerSlow),
782            "pulse" => Ok(LightFlag::Pulse),
783            "pulseslow" => Ok(LightFlag::PulseSlow),
784            "none" => Ok(LightFlag::None),
785            _ => Err(ParseLightError::UnknownVariant(s.to_string())),
786        }
787    }
788}
789
790#[cfg(test)]
791mod tests {
792    use super::*;
793
794    #[test]
795    fn parses_cli_light_override_fixed_fields() {
796        let (id, data) = parse_light_override(
797            "Torch_001=radius=255,duration=1200,red=10,green=20,blue=30,flag=FLICKERSLOW",
798        )
799        .unwrap();
800
801        assert_eq!(id, "Torch_001");
802        assert_eq!(data.radius, Some(255));
803        assert_eq!(data.duration, Some(1200.0));
804        assert_eq!(data.color, Some([10, 20, 30, 0]));
805        assert!(matches!(data.flag, Some(LightFlag::FlickerSlow)));
806    }
807
808    #[test]
809    fn parses_cli_light_override_multiplier_fields() {
810        let (_, data) = parse_light_override(
811            "Torch_002=radius_mult=2.0,duration_mult=3.0,hue_mult=4.0,saturation_mult=0.5,value_mult=0.25,red_mult=1.1,green_mult=0.8,blue_mult=0.25",
812        )
813        .unwrap();
814
815        assert_eq!(data.radius_mult, Some(2.0));
816        assert_eq!(data.duration_mult, Some(3.0));
817        assert_eq!(data.red_mult, Some(1.1));
818        assert_eq!(data.green_mult, Some(0.8));
819        assert_eq!(data.blue_mult, Some(0.25));
820        assert_eq!(data.hue_mult, Some(4.0));
821        assert_eq!(data.saturation_mult, Some(0.5));
822        assert_eq!(data.value_mult, Some(0.25));
823    }
824
825    #[test]
826    fn parses_cli_light_override_fixed_hsv_fields() {
827        let (_, data) = parse_light_override("Torch=hue=999,saturation=2.0,value=0.25").unwrap();
828
829        assert_eq!(data.hue, Some(360));
830        assert_eq!(data.saturation, Some(1.0));
831        assert_eq!(data.value, Some(0.25));
832    }
833
834    #[test]
835    fn cli_light_override_rejects_fixed_and_multiplier_for_same_field() {
836        let err = parse_light_override("Torch=radius=10,radius_mult=2.0").unwrap_err();
837
838        assert!(matches!(
839            err,
840            ParseLightError::ExclusiveFields("radius", "radius_mult")
841        ));
842    }
843
844    #[test]
845    fn cli_light_override_rejects_incomplete_rgb_color() {
846        let err = parse_light_override("Torch=red=255,green=128").unwrap_err();
847
848        assert!(matches!(err, ParseLightError::IncompleteRgb));
849    }
850
851    #[test]
852    fn cli_light_override_rejects_out_of_range_rgb_component() {
853        let err = parse_light_override("Torch=red=999,green=128,blue=64").unwrap_err();
854
855        assert!(matches!(err, ParseLightError::BadNumber("red", _)));
856    }
857
858    #[test]
859    fn cli_light_override_allows_fixed_rgb_with_hsv_multiplier() {
860        let (_, data) =
861            parse_light_override("Torch=red=255,green=128,blue=64,hue_mult=2.0,red_mult=0.5")
862                .unwrap();
863
864        assert_eq!(data.color, Some([255, 128, 64, 0]));
865        assert_eq!(data.hue_mult, Some(2.0));
866        assert_eq!(data.red_mult, Some(0.5));
867    }
868
869    #[test]
870    fn cli_light_override_rejects_hsv_fixed_and_multiplier_in_both_orders() {
871        for raw in [
872            "Torch=hue=10,hue_mult=2.0",
873            "Torch=hue_mult=2.0,hue=10",
874            "Torch=saturation=0.5,saturation_mult=2.0",
875            "Torch=saturation_mult=2.0,saturation=0.5",
876            "Torch=value=0.5,value_mult=2.0",
877            "Torch=value_mult=2.0,value=0.5",
878        ] {
879            assert!(parse_light_override(raw).is_err(), "{raw}");
880        }
881    }
882
883    #[test]
884    fn cli_light_override_rejects_complete_hsv_with_hsv_multiplier() {
885        let err = parse_light_override("Torch=hue=180,saturation=1.0,value=1.0,hue_mult=2.0")
886            .unwrap_err();
887
888        assert!(matches!(
889            err,
890            ParseLightError::ExclusiveFields("hue", "hue_mult")
891        ));
892    }
893
894    #[test]
895    fn cli_light_override_rejects_non_finite_rgb_multiplier() {
896        let err = parse_light_override("Torch=red_mult=NaN").unwrap_err();
897
898        assert!(matches!(err, ParseLightError::BadNumber("red_mult", _)));
899    }
900
901    #[test]
902    fn parses_cli_ambient_override_all_fields() {
903        let (id, ambient) = parse_ambient_override(
904            "caius=ambient=red=10,green=20,blue=30;sunlight=red=40,green=50,blue=60;fog=red=70,green=80,blue=90;fog_density=0.25",
905        )
906        .unwrap();
907
908        assert_eq!(id, "caius");
909        assert_eq!(
910            ambient.ambient.as_ref().unwrap().to_esp_color(),
911            [10, 20, 30, 0]
912        );
913        assert_eq!(
914            ambient.sunlight.as_ref().unwrap().to_esp_color(),
915            [40, 50, 60, 0]
916        );
917        assert_eq!(
918            ambient.fog.as_ref().unwrap().to_esp_color(),
919            [70, 80, 90, 0]
920        );
921        assert_eq!(ambient.fog_density, Some(0.25));
922    }
923
924    #[test]
925    fn cli_ambient_override_reports_bad_nested_color() {
926        let err = parse_ambient_override("caius=ambient=red=30,green=50").unwrap_err();
927
928        assert!(matches!(err, ParseAmbientError::BadColor(field, _) if field == "ambient"));
929    }
930
931    #[test]
932    fn cli_ambient_override_rejects_unknown_fields() {
933        let err = parse_ambient_override("caius=glow=red=30,green=50,blue=60").unwrap_err();
934
935        assert!(matches!(err, ParseAmbientError::UnknownField(field) if field == "glow"));
936    }
937
938    #[test]
939    fn toml_light_data_rejects_fixed_and_multiplier_for_same_field() {
940        let err = toml::from_str::<CustomLightData>("radius = 10\nradius_mult = 2.0").unwrap_err();
941
942        assert!(err.to_string().contains("mutually exclusive"));
943    }
944
945    #[test]
946    fn toml_typed_light_color_uses_rgb_components() {
947        let color = toml::from_str::<TypedLightColor>("red = 10\ngreen = 20\nblue = 30").unwrap();
948
949        assert_eq!(color.to_esp_color(), [10, 20, 30, 0]);
950    }
951
952    #[test]
953    fn toml_typed_light_color_rejects_out_of_range_rgb_component() {
954        let err =
955            toml::from_str::<TypedLightColor>("red = 256\ngreen = 20\nblue = 30").unwrap_err();
956
957        assert!(err.to_string().contains("invalid value"));
958    }
959
960    #[test]
961    fn toml_legacy_hsv_light_color_is_preserved() {
962        let data = toml::from_str::<CustomLightData>(
963            "hue = 180\nsaturation = 1.0\nvalue = 1.0\nradius = 100",
964        )
965        .unwrap();
966
967        assert_eq!(data.color, None);
968        assert_eq!(data.hue, Some(180));
969        assert_eq!(data.saturation, Some(1.0));
970        assert_eq!(data.value, Some(1.0));
971    }
972
973    #[test]
974    fn toml_partial_legacy_hsv_light_color_is_preserved() {
975        let data = toml::from_str::<CustomLightData>("hue = 999\nsaturation = 2.0\n").unwrap();
976
977        assert_eq!(data.color, None);
978        assert_eq!(data.hue, Some(360));
979        assert_eq!(data.saturation, Some(1.0));
980        assert_eq!(data.value, None);
981    }
982
983    #[test]
984    fn toml_complete_hsv_with_hsv_multiplier_rejects_same_component_conflict() {
985        let err = toml::from_str::<CustomLightData>(
986            "hue = 180\nsaturation = 1.0\nvalue = 1.0\nhue_mult = 2.0",
987        )
988        .unwrap_err();
989
990        assert!(err.to_string().contains("mutually exclusive"));
991    }
992
993    #[test]
994    fn toml_light_data_rejects_non_finite_rgb_multiplier() {
995        let err = toml::from_str::<CustomLightData>("red_mult = nan").unwrap_err();
996
997        assert!(err.to_string().contains("red_mult must be finite"));
998    }
999
1000    #[test]
1001    fn toml_legacy_hsv_ambient_color_is_still_accepted_as_rgb() {
1002        let color =
1003            toml::from_str::<TypedLightColor>("hue = 120\nsaturation = 1.0\nvalue = 1.0").unwrap();
1004
1005        assert_eq!(color.to_esp_color(), [0, 255, 0, 0]);
1006    }
1007
1008    #[test]
1009    fn toml_light_data_serializes_rgb_as_named_components() {
1010        let serialized = toml::to_string(&CustomLightData {
1011            color: Some([10, 20, 30, 0]),
1012            red_mult: Some(1.5),
1013            radius: Some(100),
1014            ..CustomLightData::default()
1015        })
1016        .unwrap();
1017
1018        assert!(serialized.contains("red = 10"));
1019        assert!(serialized.contains("green = 20"));
1020        assert!(serialized.contains("blue = 30"));
1021        assert!(serialized.contains("red_mult = 1.5"));
1022        assert!(!serialized.contains("color"));
1023    }
1024
1025    #[test]
1026    fn toml_light_data_allows_rgb_and_hsv_multipliers_together() {
1027        let data = toml::from_str::<CustomLightData>(
1028            "red = 10\ngreen = 20\nblue = 30\nhue_mult = 2.0\nred_mult = 0.5",
1029        )
1030        .unwrap();
1031
1032        assert_eq!(data.color, Some([10, 20, 30, 0]));
1033        assert_eq!(data.hue_mult, Some(2.0));
1034        assert_eq!(data.red_mult, Some(0.5));
1035    }
1036
1037    #[test]
1038    fn toml_light_flag_accepts_documented_uppercase_names() {
1039        #[derive(Deserialize)]
1040        struct FlagWrapper {
1041            flag: LightFlag,
1042        }
1043
1044        for (raw, expected) in [
1045            ("FLICKERSLOW", LightFlag::FlickerSlow),
1046            ("FLICKER", LightFlag::Flicker),
1047            ("PULSE", LightFlag::Pulse),
1048            ("PULSESLOW", LightFlag::PulseSlow),
1049            ("NONE", LightFlag::None),
1050        ] {
1051            let parsed = toml::from_str::<FlagWrapper>(&format!("flag = '{raw}'")).unwrap();
1052
1053            assert!(std::mem::discriminant(&parsed.flag) == std::mem::discriminant(&expected));
1054        }
1055    }
1056}