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