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 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)]
558pub 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 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}