1use std::{borrow::Cow, fmt};
5
6use crate::{ConfigError, GameSetting, GameSettingMeta, bail_config};
7
8#[derive(Debug, Clone)]
10pub struct ColorGameSetting {
11 meta: GameSettingMeta,
12 key: String,
13 value: (u8, u8, u8),
14 raw_value: String,
15}
16
17impl std::fmt::Display for ColorGameSetting {
18 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
19 write!(
20 f,
21 "{}fallback={},{}",
22 self.meta.comment, self.key, self.raw_value
23 )
24 }
25}
26
27#[derive(Debug, Clone)]
29pub struct StringGameSetting {
30 meta: GameSettingMeta,
31 key: String,
32 value: String,
33}
34
35impl std::fmt::Display for StringGameSetting {
36 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37 write!(
38 f,
39 "{}fallback={},{}",
40 self.meta.comment, self.key, self.value
41 )
42 }
43}
44
45#[derive(Debug, Clone)]
47pub struct FloatGameSetting {
48 meta: GameSettingMeta,
49 key: String,
50 value: f64,
51 raw_value: String,
52}
53
54impl std::fmt::Display for FloatGameSetting {
55 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56 write!(
57 f,
58 "{}fallback={},{}",
59 self.meta.comment, self.key, self.raw_value
60 )
61 }
62}
63
64#[derive(Debug, Clone)]
66pub struct IntGameSetting {
67 meta: GameSettingMeta,
68 key: String,
69 value: i64,
70 raw_value: String,
71}
72
73impl std::fmt::Display for IntGameSetting {
74 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
75 write!(
76 f,
77 "{}fallback={},{}",
78 self.meta.comment, self.key, self.raw_value
79 )
80 }
81}
82
83#[derive(Debug, Clone)]
94#[non_exhaustive]
95pub enum GameSettingType {
96 Color(ColorGameSetting),
98 String(StringGameSetting),
100 Float(FloatGameSetting),
102 Int(IntGameSetting),
104}
105
106impl GameSettingType {
107 #[must_use]
118 pub fn key(&self) -> &String {
119 match self {
120 GameSettingType::Color(setting) => &setting.key,
121 GameSettingType::String(setting) => &setting.key,
122 GameSettingType::Float(setting) => &setting.key,
123 GameSettingType::Int(setting) => &setting.key,
124 }
125 }
126
127 #[must_use]
129 pub fn key_str(&self) -> &str {
130 match self {
131 GameSettingType::Color(setting) => &setting.key,
132 GameSettingType::String(setting) => &setting.key,
133 GameSettingType::Float(setting) => &setting.key,
134 GameSettingType::Int(setting) => &setting.key,
135 }
136 }
137
138 #[must_use]
149 pub fn value(&self) -> Cow<'_, str> {
150 match self {
151 GameSettingType::Color(setting) => {
152 let _ = setting.value;
153 Cow::Borrowed(&setting.raw_value)
154 }
155 GameSettingType::String(setting) => Cow::Borrowed(&setting.value),
156 GameSettingType::Float(setting) => {
157 let _ = setting.value;
158 Cow::Borrowed(&setting.raw_value)
159 }
160 GameSettingType::Int(setting) => {
161 let _ = setting.value;
162 Cow::Borrowed(&setting.raw_value)
163 }
164 }
165 }
166}
167
168impl std::fmt::Display for GameSettingType {
169 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
170 match self {
171 GameSettingType::Color(s) => write!(f, "{s}"),
172 GameSettingType::Float(s) => write!(f, "{s}"),
173 GameSettingType::String(s) => write!(f, "{s}"),
174 GameSettingType::Int(s) => write!(f, "{s}"),
175 }
176 }
177}
178
179impl GameSetting for GameSettingType {
180 fn meta(&self) -> &GameSettingMeta {
181 match self {
182 GameSettingType::Color(s) => &s.meta,
183 GameSettingType::String(s) => &s.meta,
184 GameSettingType::Float(s) => &s.meta,
185 GameSettingType::Int(s) => &s.meta,
186 }
187 }
188}
189
190impl PartialEq for GameSettingType {
191 fn eq(&self, other: &Self) -> bool {
192 use GameSettingType::{Color, Float, Int, String};
193
194 match (self, other) {
195 (Color(a), Color(b)) => a.key == b.key,
196 (String(a), String(b)) => a.key == b.key,
197 (Float(a), Float(b)) => a.key == b.key,
198 (Int(a), Int(b)) => a.key == b.key,
199 _ => false,
201 }
202 }
203}
204
205impl PartialEq<&str> for GameSettingType {
206 fn eq(&self, other: &&str) -> bool {
207 use GameSettingType::{Color, Float, Int, String};
208
209 match self {
210 Color(a) => a.key == *other,
211 String(a) => a.key == *other,
212 Float(a) => a.key == *other,
213 Int(a) => a.key == *other,
214 }
215 }
216}
217
218impl Eq for GameSettingType {}
219
220impl TryFrom<(String, std::path::PathBuf, &mut String)> for GameSettingType {
221 type Error = ConfigError;
222
223 fn try_from(
224 (original_value, source_config, queued_comment): (String, std::path::PathBuf, &mut String),
225 ) -> Result<Self, ConfigError> {
226 let Some((key, value)) = original_value.split_once(',') else {
227 bail_config!(invalid_game_setting, original_value, source_config);
228 };
229
230 let key = key.to_string();
231 let value = value.to_string();
232
233 let meta = GameSettingMeta {
234 source_config,
235 comment: queued_comment.clone(),
236 };
237
238 queued_comment.clear();
239
240 if let Some(color) = parse_color_value(&value) {
241 return Ok(GameSettingType::Color(ColorGameSetting {
242 meta,
243 key,
244 value: color,
245 raw_value: value,
246 }));
247 }
248
249 if value.contains('.')
250 && let Ok(f) = value.parse::<f64>()
251 {
252 return Ok(GameSettingType::Float(FloatGameSetting {
253 meta,
254 key,
255 value: f,
256 raw_value: value,
257 }));
258 }
259
260 if let Ok(i) = value.parse::<i64>() {
261 return Ok(GameSettingType::Int(IntGameSetting {
262 meta,
263 key,
264 value: i,
265 raw_value: value,
266 }));
267 }
268
269 Ok(GameSettingType::String(StringGameSetting {
270 meta,
271 key,
272 value,
273 }))
274 }
275}
276
277fn parse_color_value(value: &str) -> Option<(u8, u8, u8)> {
278 let mut parts = value.split(',').map(str::trim);
279 let r = parts.next()?.parse::<u8>().ok()?;
280 let g = parts.next()?.parse::<u8>().ok()?;
281 let b = parts.next()?.parse::<u8>().ok()?;
282
283 if parts.next().is_some() {
284 return None;
285 }
286
287 Some((r, g, b))
288}
289
290#[cfg(test)]
291mod tests {
292 use std::path::PathBuf;
293
294 use super::*;
295
296 fn default_meta() -> GameSettingMeta {
297 GameSettingMeta {
298 source_config: PathBuf::default(),
299 comment: String::default(),
300 }
301 }
302
303 #[test]
304 fn test_value_string_setting() {
305 let setting = GameSettingType::String(StringGameSetting {
306 meta: default_meta(),
307 key: "greeting".into(),
308 value: "hello world".into(),
309 });
310
311 assert_eq!(setting.value(), "hello world");
312 }
313
314 #[test]
315 fn test_value_int_setting() {
316 let setting = GameSettingType::Int(IntGameSetting {
317 meta: default_meta(),
318 key: "MaxEyesOfTodd".into(),
319 value: 3,
320 raw_value: "3".into(),
321 });
322
323 assert_eq!(setting.value(), "3");
324 }
325
326 #[test]
327 fn test_value_float_setting() {
328 let setting = GameSettingType::Float(FloatGameSetting {
329 meta: default_meta(),
330 key: "FLightAttenuationEnfuckulation".into(),
331 value: 0.75,
332 raw_value: "0.75".into(),
333 });
334
335 assert_eq!(setting.value(), "0.75");
336 }
337
338 #[test]
339 fn test_value_color_setting() {
340 let setting = GameSettingType::Color(ColorGameSetting {
341 meta: default_meta(),
342 key: "hud_color".into(),
343 value: (255, 128, 64),
344 raw_value: "255,128,64".into(),
345 });
346
347 assert_eq!(setting.value(), "255,128,64");
348 }
349
350 #[test]
351 fn test_to_string_for_string_setting() {
352 let setting = GameSettingType::String(StringGameSetting {
353 meta: default_meta(),
354 key: "sGreeting".into(),
355 value: "Hello, Nerevar.".into(),
356 });
357
358 assert_eq!(setting.to_string(), "fallback=sGreeting,Hello, Nerevar.");
359 }
360
361 #[test]
362 fn test_to_string_for_int_setting() {
363 let setting = GameSettingType::Int(IntGameSetting {
364 meta: default_meta(),
365 key: "iMaxSpeed".into(),
366 value: 42,
367 raw_value: "42".into(),
368 });
369
370 assert_eq!(setting.to_string(), "fallback=iMaxSpeed,42");
371 }
372
373 #[test]
374 fn test_to_string_for_float_setting() {
375 let setting = GameSettingType::Float(FloatGameSetting {
376 meta: default_meta(),
377 key: "fJumpHeight".into(),
378 value: 1.75,
379 raw_value: "1.75".into(),
380 });
381
382 assert_eq!(setting.to_string(), "fallback=fJumpHeight,1.75");
383 }
384
385 #[test]
386 fn test_to_string_for_color_setting() {
387 let setting = GameSettingType::Color(ColorGameSetting {
388 meta: default_meta(),
389 key: "iHUDColor".into(),
390 value: (128, 64, 255),
391 raw_value: "128,64,255".into(),
392 });
393
394 assert_eq!(setting.to_string(), "fallback=iHUDColor,128,64,255");
395 }
396
397 #[test]
398 fn test_commented_string() {
399 let setting = GameSettingType::Color(ColorGameSetting {
400 meta: GameSettingMeta {
401 source_config: PathBuf::from("$HOME/.config/openmw/openmw.cfg"),
402 comment: String::from("#Monochrome UI Settings\n#\n#\n#\n#######\n##\n##\n##\n"),
403 },
404 key: "iHUDColor".into(),
405 value: (128, 64, 255),
406 raw_value: "128,64,255".into(),
407 });
408
409 assert_eq!(
410 setting.to_string(),
411 "#Monochrome UI Settings\n#\n#\n#\n#######\n##\n##\n##\nfallback=iHUDColor,128,64,255"
412 );
413 }
414
415 fn parse(s: &str) -> Result<GameSettingType, crate::ConfigError> {
418 GameSettingType::try_from((s.to_string(), PathBuf::default(), &mut String::new()))
419 }
420
421 #[test]
422 fn test_parse_string_value() {
423 let setting = parse("sMyKey,hello world").unwrap();
424 assert!(matches!(setting, GameSettingType::String(_)));
425 assert_eq!(setting.key(), "sMyKey");
426 assert_eq!(setting.key_str(), "sMyKey");
427 assert_eq!(setting.value(), "hello world");
428 }
429
430 #[test]
431 fn test_parse_integer_value() {
432 let setting = parse("iSpeed,42").unwrap();
433 assert!(matches!(setting, GameSettingType::Int(_)));
434 assert_eq!(setting.value(), "42");
435 }
436
437 #[test]
438 fn test_parse_negative_integer() {
439 let setting = parse("iDepth,-100").unwrap();
440 assert!(matches!(setting, GameSettingType::Int(_)));
441 assert_eq!(setting.value(), "-100");
442 }
443
444 #[test]
445 fn test_parse_float_value() {
446 let setting = parse("fGravity,9.81").unwrap();
447 assert!(matches!(setting, GameSettingType::Float(_)));
448 assert_eq!(setting.value(), "9.81");
449 }
450
451 #[test]
452 fn test_parse_color_value() {
453 let setting = parse("iSkyColor,100,149,237").unwrap();
454 assert!(matches!(setting, GameSettingType::Color(_)));
455 assert_eq!(setting.value(), "100,149,237");
456 }
457
458 #[test]
459 fn test_parse_missing_comma_errors() {
460 assert!(parse("NoCommaAtAll").is_err());
461 }
462
463 #[test]
464 fn test_parse_value_with_comma_stays_string() {
465 let setting = parse("sMessage,Hello, traveller").unwrap();
467 assert!(matches!(setting, GameSettingType::String(_)));
468 assert_eq!(setting.value(), "Hello, traveller");
469 }
470
471 #[test]
472 fn test_parse_ambiguous_two_number_value_is_string() {
473 let setting = parse("sKey,10,20").unwrap();
475 assert!(matches!(setting, GameSettingType::String(_)));
476 }
477
478 #[test]
479 fn test_parse_color_out_of_u8_range_is_string() {
480 let setting = parse("sBig,256,0,0").unwrap();
482 assert!(matches!(setting, GameSettingType::String(_)));
483 }
484
485 #[test]
486 fn test_parse_comment_consumed() {
487 let mut comment = String::from("# some note\n");
488 let setting =
489 GameSettingType::try_from(("iVal,1".to_string(), PathBuf::default(), &mut comment))
490 .unwrap();
491 assert_eq!(setting.meta().comment, "# some note\n");
492 assert!(comment.is_empty(), "comment should be consumed");
493 }
494
495 #[test]
498 fn test_same_key_same_type_are_equal() {
499 let a = parse("iKey,1").unwrap();
500 let b = parse("iKey,2").unwrap();
501 assert_eq!(a, b, "equality is key-only within the same type");
502 }
503
504 #[test]
505 fn test_different_keys_not_equal() {
506 let a = parse("iKey,1").unwrap();
507 let b = parse("iOther,1").unwrap();
508 assert_ne!(a, b);
509 }
510
511 #[test]
512 fn test_mismatched_types_not_equal() {
513 let int_setting = parse("iKey,1").unwrap();
515 let float_setting = parse("iKey,1.0").unwrap();
516 assert_ne!(int_setting, float_setting);
517 }
518
519 #[test]
520 fn test_eq_with_str_key() {
521 let setting = parse("iMaxLevel,50").unwrap();
522 assert_eq!(setting, "iMaxLevel");
523 assert_ne!(setting, "iOtherKey");
524 }
525}