1use crate::prelude::*;
5
6use alloc::collections::BTreeMap;
7
8use serde::{Deserialize, Serialize};
9
10use crate::error::ContentError;
11use crate::format::{AssetKind, AssetPath, DecodeCx, Format, TEXT_MAX_BYTES, text};
12use crate::grid_map::DEFAULT_LIGHT;
13use crate::ron::parse_ron;
14
15impl AssetKind for Lighting {
16 const NAME: &'static str = "lights";
17}
18
19#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct LightingRon {
23 default: String,
24}
25
26impl LightingRon {
27 pub fn new(default: impl Into<String>) -> Self {
29 Self {
30 default: default.into(),
31 }
32 }
33}
34
35impl Default for LightingRon {
36 fn default() -> Self {
38 Self::new(DEFAULT_LIGHT)
39 }
40}
41
42impl Format for LightingRon {
43 type Asset = Lighting;
44
45 fn id(&self) -> &'static str {
46 "lights"
47 }
48
49 fn extensions(&self) -> &'static [&'static str] {
50 &["ron"]
51 }
52
53 fn max_bytes(&self) -> usize {
54 TEXT_MAX_BYTES
55 }
56
57 fn decode(
58 &self,
59 file: &AssetPath,
60 bytes: &[u8],
61 _cx: &DecodeCx<'_>,
62 ) -> Result<Lighting, Vec<ContentError>> {
63 from_source(file.shown(), text(file, bytes)?, &self.default)
64 }
65}
66
67const PARTS: u16 = 1000;
70
71#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
76pub struct Light {
77 pub grey: u16,
79 pub tint: [u16; 3],
82}
83
84impl Light {
85 pub const PLAIN: Self = Self {
87 grey: 0,
88 tint: [PARTS; 3],
89 };
90
91 pub fn new(grey: f32, tint: [f32; 3]) -> Self {
94 Self {
95 grey: parts(grey),
96 tint: tint.map(parts),
97 }
98 }
99
100 pub fn is_plain(self) -> bool {
102 self == Self::PLAIN
103 }
104
105 pub fn apply(self, rgb: [u8; 3]) -> [u8; 3] {
108 let [r, g, b] = rgb.map(f32::from);
109 let own_grey = 0.299 * r + 0.587 * g + 0.114 * b;
110 let grey = fraction(self.grey);
111 let shade = |c: f32, tint: u16| to_channel((c + (own_grey - c) * grey) * fraction(tint));
112 [
113 shade(r, self.tint[0]),
114 shade(g, self.tint[1]),
115 shade(b, self.tint[2]),
116 ]
117 }
118}
119
120impl Default for Light {
121 fn default() -> Self {
122 Self::PLAIN
123 }
124}
125
126#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] fn parts(v: f32) -> u16 {
129 libm::roundf(v * f32::from(PARTS)).clamp(0.0, f32::from(PARTS)) as u16
130}
131
132fn fraction(parts: u16) -> f32 {
134 f32::from(parts) / f32::from(PARTS)
135}
136
137#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] fn to_channel(v: f32) -> u8 {
140 libm::roundf(v).clamp(0.0, 255.0) as u8
141}
142
143#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
145pub struct Lighting {
146 pub lights: BTreeMap<String, Light>,
148}
149
150impl Lighting {
151 pub fn get(&self, name: &str) -> Option<Light> {
153 self.lights.get(name).copied()
154 }
155
156 pub fn names(&self) -> String {
159 let quoted: Vec<String> = self.lights.keys().map(|l| format!("\"{l}\"")).collect();
160 quoted.join(", ")
161 }
162}
163
164#[derive(Deserialize)]
165#[serde(deny_unknown_fields)]
166struct RawLight {
167 grey: f32,
168 tint: (f32, f32, f32),
169}
170
171pub fn from_source(file: &str, source: &str, default: &str) -> Result<Lighting, Vec<ContentError>> {
175 let raw: BTreeMap<String, RawLight> = parse_ron(file, source).map_err(|e| vec![e])?;
176 let mut errors = Vec::new();
177 let mut lights = BTreeMap::new();
178 for (name, light) in raw {
179 let (r, g, b) = light.tint;
180 let numbers = [
181 ("grey", light.grey),
182 ("tint red", r),
183 ("tint green", g),
184 ("tint blue", b),
185 ];
186 for (what, v) in numbers {
187 if !(0.0..=1.0).contains(&v) {
188 let message = format!("light \"{name}\": {what} is {v}; it must be from 0 to 1");
189 errors.push(ContentError::new(file, message));
190 }
191 }
192 lights.insert(name, Light::new(light.grey, [r, g, b]));
193 }
194 if !lights.contains_key(default) {
195 let message = format!("no light \"{default}\": it is the light of a map that names none");
196 errors.push(ContentError::new(file, message));
197 }
198 if errors.is_empty() {
199 Ok(Lighting { lights })
200 } else {
201 Err(errors)
202 }
203}
204
205#[cfg(test)]
206mod tests {
207 use super::*;
208
209 const C3: Light = Light {
211 grey: 350,
212 tint: [620, 560, 460],
213 };
214
215 #[test]
216 fn c3s_light_on_three_colours() {
217 assert_eq!(C3.apply([96, 168, 72]), [68, 88, 43]);
220 assert_eq!(C3.apply([64, 144, 208]), [53, 77, 83]);
222 assert_eq!(C3.apply([224, 200, 96]), [133, 111, 60]);
224 assert_eq!(C3.apply([255, 255, 255]), [158, 143, 117]);
226 assert_eq!(C3.apply([0, 0, 0]), [0, 0, 0]);
227 }
228
229 #[test]
230 fn grey_and_tint_each_do_their_part() {
231 let grey = Light {
232 grey: 1000,
233 ..Light::PLAIN
234 };
235 assert_eq!(grey.apply([255, 0, 0]), [76, 76, 76]);
237 let half = Light {
238 grey: 500,
239 ..Light::PLAIN
240 };
241 assert_eq!(half.apply([255, 0, 0]), [166, 38, 38]);
242 let tint = Light {
243 grey: 0,
244 tint: [1000, 500, 0],
245 };
246 assert_eq!(tint.apply([200, 200, 200]), [200, 100, 0]);
247 }
248
249 #[test]
250 fn the_plain_light_changes_nothing() {
251 assert!(Light::PLAIN.is_plain());
252 assert!(Light::default().is_plain());
253 assert!(!C3.is_plain());
254 for rgb in [[0, 0, 0], [255, 255, 255], [1, 2, 3], [96, 168, 72]] {
255 assert_eq!(Light::PLAIN.apply(rgb), rgb);
256 }
257 }
258
259 #[test]
260 fn a_light_keeps_thousandths() {
261 assert_eq!(Light::new(0.35, [0.62, 0.56, 0.46]), C3);
262 assert_eq!(Light::new(0.0, [1.0; 3]), Light::PLAIN);
263 assert_eq!(Light::new(0.0004, [0.9996, 0.5, 0.5]).grey, 0);
264 assert_eq!(Light::new(0.0006, [0.9996, 0.5, 0.5]).tint[0], 1000);
265 let cut = Light::new(-1.0, [2.0, f32::NAN, 0.25]);
267 assert_eq!((cut.grey, cut.tint), (0, [1000, 0, 250]));
268 }
269
270 const VALID: &str = r#"{
271 "noon": (grey: 0.0, tint: (1.0, 1.0, 1.0)),
272 "dusk": (grey: 0.35, tint: (0.62, 0.56, 0.46)),
273 }"#;
274
275 #[test]
276 fn a_valid_file_loads() {
277 let lighting = from_source("l.ron", VALID, "noon").unwrap();
278 assert_eq!(lighting.lights.len(), 2);
279 assert_eq!(lighting.get("noon"), Some(Light::PLAIN));
280 assert_eq!(lighting.get("dusk"), Some(C3));
281 assert_eq!(lighting.get("night"), None);
282 assert_eq!(lighting.names(), "\"dusk\", \"noon\"");
283 }
284
285 fn messages(source: &str) -> Vec<String> {
286 let errors = from_source("l.ron", source, "noon")
287 .err()
288 .unwrap_or_default();
289 errors
290 .into_iter()
291 .map(|e| {
292 assert_eq!(e.file, "l.ron");
293 e.message
294 })
295 .collect()
296 }
297
298 #[test]
299 fn every_number_must_be_from_0_to_1() {
300 let source = r#"{
301 "noon": (grey: 0.0, tint: (1.0, 1.0, 1.0)),
302 "odd": (grey: 1.5, tint: (-0.1, 1.001, 2.0)),
303 }"#;
304 assert_eq!(
305 messages(source),
306 [
307 "light \"odd\": grey is 1.5; it must be from 0 to 1",
308 "light \"odd\": tint red is -0.1; it must be from 0 to 1",
309 "light \"odd\": tint green is 1.001; it must be from 0 to 1",
310 "light \"odd\": tint blue is 2; it must be from 0 to 1",
311 ]
312 );
313 let ends = r#"{"noon": (grey: 1.0, tint: (0.0, 0.0, 1.0))}"#;
315 assert_eq!(messages(ends), [""; 0]);
316 }
317
318 #[test]
319 fn the_default_light_must_exist() {
320 let source = r#"{"dusk": (grey: 0.35, tint: (0.62, 0.56, 0.46))}"#;
321 assert_eq!(
322 messages(source),
323 ["no light \"noon\": it is the light of a map that names none"]
324 );
325 assert_eq!(messages("{}").len(), 1);
326 assert!(from_source("l.ron", source, "dusk").is_ok());
328 }
329
330 #[test]
331 fn a_broken_file_is_reported_where_it_breaks() {
332 let errors = from_source("l.ron", "{\n \"noon\": (grey: 0.0),\n}", "noon")
333 .err()
334 .unwrap_or_default();
335 assert_eq!(errors.len(), 1);
336 assert_eq!(errors[0].line, Some(2));
337 let extra = VALID.replace("grey: 0.0,", "grey: 0.0, glow: 1.0,");
339 assert!(from_source("l.ron", &extra, "noon").is_err());
340 }
341}