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