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