1use merg::Merge;
2use num_traits::FloatConst;
3use serde::Deserialize;
4use serde_with::{serde_as, DeserializeFromStr, DurationSeconds};
5use std::collections::HashMap;
6use std::ffi::{c_char, c_int};
7use std::io;
8use std::path::{Path, PathBuf};
9use std::str::FromStr;
10use std::time::Duration;
11use thiserror::Error;
12use tracing::info;
13use validator::Validate;
14use video::aspect::AspectRatio;
15use video::resolution::Resolution;
16
17mod bootcore;
18mod fb_size;
19mod hdmi_limited;
20mod hdr;
21mod ini; mod ntsc_mode;
23mod osd_rotate;
24mod reset_combo;
25mod vga_mode;
26pub mod video;
27mod vrr_mode;
28mod vscale_mode;
29mod vsync_adjust;
30
31pub use bootcore::*;
32pub use fb_size::*;
33pub use hdmi_limited::*;
34pub use hdr::*;
35pub use ntsc_mode::*;
36pub use osd_rotate::*;
37pub use reset_combo::*;
38pub use vga_mode::*;
39pub use video::*;
40pub use vrr_mode::*;
41pub use vscale_mode::*;
42pub use vsync_adjust::*;
43
44mod cpp {
45 use libc::*;
46
47 #[repr(C)]
48 pub struct CppCfg {
49 pub keyrah_mode: u32,
50 pub forced_scandoubler: u8,
51 pub key_menu_as_rgui: u8,
52 pub reset_combo: u8,
53 pub csync: u8,
54 pub vga_scaler: u8,
55 pub vga_sog: u8,
56 pub hdmi_audio_96k: u8,
57 pub dvi_mode: u8,
58 pub hdmi_limited: u8,
59 pub direct_video: u8,
60 pub video_info: u8,
61 pub refresh_min: c_float,
62 pub refresh_max: c_float,
63 pub controller_info: u8,
64 pub vsync_adjust: u8,
65 pub kbd_nomouse: u8,
66 pub mouse_throttle: u8,
67 pub bootscreen: u8,
68 pub vscale_mode: u8,
69 pub vscale_border: u16,
70 pub rbf_hide_datecode: u8,
71 pub menu_pal: u8,
72 pub bootcore_timeout: i16,
73 pub fb_size: u8,
74 pub fb_terminal: u8,
75 pub osd_rotate: u8,
76 pub osd_timeout: u16,
77 pub gamepad_defaults: u8,
78 pub recents: u8,
79 pub jamma_vid: u16,
80 pub jamma_pid: u16,
81 pub no_merge_vid: u16,
82 pub no_merge_pid: u16,
83 pub no_merge_vidpid: [u32; 256],
84 pub spinner_vid: u16,
85 pub spinner_pid: u16,
86 pub spinner_throttle: c_int,
87 pub spinner_axis: u8,
88 pub sniper_mode: u8,
89 pub browse_expand: u8,
90 pub logo: u8,
91 pub log_file_entry: u8,
92 pub shmask_mode_default: u8,
93 pub bt_auto_disconnect: c_int,
94 pub bt_reset_before_pair: c_int,
95 pub bootcore: [c_char; 256],
96 pub video_conf: [c_char; 1024],
97 pub video_conf_pal: [c_char; 1024],
98 pub video_conf_ntsc: [c_char; 1024],
99 pub font: [c_char; 1024],
100 pub shared_folder: [c_char; 1024],
101 pub waitmount: [c_char; 1024],
102 pub custom_aspect_ratio: [[c_char; 16]; 2],
103 pub afilter_default: [c_char; 1023],
104 pub vfilter_default: [c_char; 1023],
105 pub vfilter_vertical_default: [c_char; 1023],
106 pub vfilter_scanlines_default: [c_char; 1023],
107 pub shmask_default: [c_char; 1023],
108 pub preset_default: [c_char; 1023],
109 pub player_controller: [[[c_char; 256]; 8]; 6],
110 pub rumble: u8,
111 pub wheel_force: u8,
112 pub wheel_range: u16,
113 pub hdmi_game_mode: u8,
114 pub vrr_mode: u8,
115 pub vrr_min_framerate: u8,
116 pub vrr_max_framerate: u8,
117 pub vrr_vesa_framerate: u8,
118 pub video_off: u16,
119 pub disable_autofire: u8,
120 pub video_brightness: u8,
121 pub video_contrast: u8,
122 pub video_saturation: u8,
123 pub video_hue: u16,
124 pub video_gain_offset: [c_char; 256],
125 pub hdr: u8,
126 pub hdr_max_nits: u16,
127 pub hdr_avg_nits: u16,
128 pub vga_mode: [c_char; 16],
129 pub vga_mode_int: c_char,
130 pub ntsc_mode: c_char,
131 pub controller_unique_mapping: [u32; 256],
132 }
133}
134
135#[derive(Error, Debug)]
136#[error(transparent)]
137pub enum ConfigError {
138 #[error("Invalid config file: {0}")]
139 Io(#[from] io::Error),
140
141 #[error("Could not read INI file: {0}")]
142 IniError(#[from] ini::Error),
143
144 #[error("Could not read JSON file: {0}")]
145 JsonError(#[from] json5::Error),
146}
147
148struct DurationMinutes;
150
151impl<'de> serde_with::DeserializeAs<'de, Duration> for DurationMinutes {
152 fn deserialize_as<D>(deserializer: D) -> Result<Duration, D::Error>
153 where
154 D: serde::Deserializer<'de>,
155 {
156 let dur: Duration = DurationSeconds::<u64>::deserialize_as(deserializer)?;
157 let secs = dur.as_secs();
158 Ok(Duration::from_secs(secs * 60))
159 }
160}
161
162#[derive(DeserializeFromStr, Debug, Clone, Hash, Ord, PartialOrd, Eq, PartialEq)]
164pub struct VideoResolutionString(Resolution);
165
166impl FromStr for VideoResolutionString {
167 type Err = &'static str;
168
169 fn from_str(s: &str) -> Result<Self, Self::Err> {
170 if let Some(s) = s.strip_prefix("video=") {
171 Ok(VideoResolutionString(Resolution::from_str(s)?))
172 } else {
173 Err("Invalid video section string.")
174 }
175 }
176}
177
178mod mister_bool {
180 use serde::{Deserialize, Deserializer};
181
182 fn bool_from_str(s: impl AsRef<str>) -> bool {
184 match s.as_ref() {
185 "enabled" | "true" | "1" => true,
186 "0" => false,
187 _ => false,
188 }
189 }
190
191 pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<bool>, D::Error>
192 where
193 D: Deserializer<'de>,
194 {
195 Option::deserialize(deserializer).map(|v: Option<String>| v.map(bool_from_str))
196 }
197}
198
199mod mister_hexa {
201 use serde::{Deserialize, Deserializer};
202
203 pub fn deserialize<'de, D, T: num_traits::Num>(deserializer: D) -> Result<Option<T>, D::Error>
204 where
205 D: Deserializer<'de>,
206 {
207 Option::deserialize(deserializer).and_then(|v: Option<String>| {
208 if let Some(s) = v {
209 if let Some(hs) = s.strip_prefix("0x") {
210 T::from_str_radix(hs, 16)
211 .map_err(|_| {
212 serde::de::Error::custom(format!("Invalid hexadecimal value: {s}"))
213 })
214 .map(Some)
215 } else if s == "0" {
216 Ok(Some(T::zero()))
217 } else {
218 Err(serde::de::Error::custom(format!(
219 "Invalid hexadecimal value: {}",
220 s
221 )))
222 }
223 } else {
224 Ok(None)
225 }
226 })
227 }
228}
229
230mod mister_hexa_seq {
232 use serde::{Deserialize, Deserializer};
233
234 pub fn deserialize<'de, D, T: num_traits::Num>(deserializer: D) -> Result<Vec<T>, D::Error>
235 where
236 D: Deserializer<'de>,
237 {
238 Vec::deserialize(deserializer).and_then(|v: Vec<String>| {
239 v.into_iter()
240 .map(|s| {
241 if let Some(hs) = s.strip_prefix("0x") {
242 T::from_str_radix(hs, 16).map_err(|_| {
243 serde::de::Error::custom(format!("Invalid hexadecimal value: {s}"))
244 })
245 } else if s == "0" {
246 Ok(T::zero())
247 } else {
248 Err(serde::de::Error::custom(format!(
249 "Invalid hexadecimal value: {}",
250 s
251 )))
252 }
253 })
254 .collect()
255 })
256 }
257}
258
259mod validate {
260 use std::time::Duration;
261 use validator::ValidationError;
262
263 pub fn video_info(video_info: &Duration) -> Result<(), ValidationError> {
264 if video_info.as_secs() > 10 || video_info.as_secs() < 1 {
265 return Err(ValidationError::new("video_info must be between 1 and 10."));
266 }
267
268 Ok(())
269 }
270
271 pub fn controller_info(controller_info: &Duration) -> Result<(), ValidationError> {
272 if controller_info.as_secs() > 10 {
273 return Err(ValidationError::new(
274 "controller_info must be between 0 and 10.",
275 ));
276 }
277
278 Ok(())
279 }
280
281 pub fn osd_timeout(osd_timeout: &Duration) -> Result<(), ValidationError> {
282 if osd_timeout.as_secs() > 3600 || osd_timeout.as_secs() < 5 {
283 return Err(ValidationError::new(
284 "osd_timeout must be between 5 and 3600.",
285 ));
286 }
287
288 Ok(())
289 }
290
291 pub fn bootcore_timeout(bootcore_timeout: &Duration) -> Result<(), ValidationError> {
292 if bootcore_timeout.as_secs() > 30 {
293 return Err(ValidationError::new("bootcore_timeout must be 30 or less."));
294 }
295
296 Ok(())
297 }
298
299 pub fn video_off(video_off: &Duration) -> Result<(), ValidationError> {
300 if video_off.as_secs() > 3600 {
301 return Err(ValidationError::new("video_off must be 3600 or less."));
302 }
303
304 Ok(())
305 }
306}
307
308#[serde_as]
318#[derive(Default, Debug, Clone, Deserialize, Merge, Validate)]
319#[serde(default)]
320pub struct MisterConfig {
321 #[merge(strategy = merg::option::overwrite_some)]
322 pub bootcore: Option<BootCoreConfig>,
323
324 #[serde(alias = "ypbpr")]
325 #[merge(strategy = merg::option::overwrite_some)]
326 pub vga_mode: Option<VgaMode>,
327
328 #[merge(strategy = merg::option::overwrite_some)]
329 pub ntsc_mode: Option<NtscModeConfig>,
330
331 #[merge(strategy = merg::option::overwrite_some)]
332 pub reset_combo: Option<ResetComboConfig>,
333
334 #[merge(strategy = merg::option::overwrite_some)]
335 pub hdmi_limited: Option<HdmiLimitedConfig>,
336
337 #[merge(strategy = merg::option::overwrite_some)]
338 #[validate(range(max = 100))]
339 pub mouse_throttle: Option<u8>,
340
341 #[serde(with = "mister_hexa")]
342 #[merge(strategy = merg::option::overwrite_some)]
343 pub keyrah_mode: Option<u32>,
344
345 #[merge(strategy = merg::vec::append)]
348 custom_aspect_ratio: Vec<AspectRatio>,
349
350 #[merge(strategy = merg::option::overwrite_some)]
353 pub custom_aspect_ratio_1: Option<AspectRatio>,
354
355 #[merge(strategy = merg::option::overwrite_some)]
358 pub custom_aspect_ratio_2: Option<AspectRatio>,
359
360 #[serde(with = "mister_bool")]
362 #[merge(strategy = merg::option::overwrite_some)]
363 pub forced_scandoubler: Option<bool>,
364
365 #[serde(with = "mister_bool")]
367 #[merge(strategy = merg::option::overwrite_some)]
368 pub key_menu_as_rgui: Option<bool>,
369
370 #[serde(with = "mister_bool", alias = "csync")]
372 #[merge(strategy = merg::option::overwrite_some)]
373 pub composite_sync: Option<bool>,
374
375 #[serde(with = "mister_bool")]
377 #[merge(strategy = merg::option::overwrite_some)]
378 pub vga_scaler: Option<bool>,
379
380 #[serde(with = "mister_bool")]
382 #[merge(strategy = merg::option::overwrite_some)]
383 pub vga_sog: Option<bool>,
384
385 #[serde(with = "mister_bool")]
387 #[merge(strategy = merg::option::overwrite_some)]
388 pub hdmi_audio_96k: Option<bool>,
389
390 #[serde(with = "mister_bool")]
392 #[merge(strategy = merg::option::overwrite_some)]
393 pub dvi_mode: Option<bool>,
394
395 #[serde(with = "mister_bool")]
397 #[merge(strategy = merg::option::overwrite_some)]
398 pub direct_video: Option<bool>,
399
400 #[serde_as(as = "Option<DurationSeconds<u64>>")]
402 #[merge(strategy = merg::option::overwrite_some)]
403 #[validate(custom(function = validate::video_info))]
404 pub video_info: Option<Duration>,
405
406 #[serde_as(as = "Option<DurationSeconds<u64>>")]
409 #[validate(custom(function = validate::controller_info))]
410 #[merge(strategy = merg::option::overwrite_some)]
411 pub controller_info: Option<Duration>,
412
413 #[validate(range(min = 0.0, max = 150.0))]
418 #[merge(strategy = merg::option::overwrite_some)]
419 pub refresh_min: Option<f32>,
420
421 #[validate(range(min = 0.0, max = 150.0))]
426 #[merge(strategy = merg::option::overwrite_some)]
427 refresh_max: Option<f32>,
428
429 #[merge(strategy = merg::option::overwrite_some)]
436 vsync_adjust: Option<VsyncAdjustConfig>,
437
438 #[serde(with = "mister_bool")]
440 #[merge(strategy = merg::option::overwrite_some)]
441 kbd_nomouse: Option<bool>,
442
443 #[serde(with = "mister_bool")]
444 #[merge(strategy = merg::option::overwrite_some)]
445 bootscreen: Option<bool>,
446
447 #[merge(strategy = merg::option::overwrite_some)]
454 vscale_mode: Option<VideoScaleModeConfig>,
455
456 #[validate(range(min = 0, max = 399))]
458 #[merge(strategy = merg::option::overwrite_some)]
459 pub vscale_border: Option<u16>,
460
461 #[serde(with = "mister_bool")]
463 #[merge(strategy = merg::option::overwrite_some)]
464 rbf_hide_datecode: Option<bool>,
465
466 #[serde(with = "mister_bool")]
468 #[merge(strategy = merg::option::overwrite_some)]
469 menu_pal: Option<bool>,
470
471 #[serde_as(as = "Option<DurationSeconds<u64>>")]
473 #[validate(custom(function = validate::bootcore_timeout))]
474 #[merge(strategy = merg::option::overwrite_some)]
475 bootcore_timeout: Option<Duration>,
476
477 #[merge(strategy = merg::option::overwrite_some)]
479 pub fb_size: Option<FramebufferSizeConfig>,
480
481 #[serde(with = "mister_bool")]
483 #[merge(strategy = merg::option::overwrite_some)]
484 fb_terminal: Option<bool>,
485
486 #[merge(strategy = merg::option::overwrite_some)]
488 osd_rotate: Option<OsdRotateConfig>,
489
490 #[serde_as(as = "Option<DurationSeconds<u64>>")]
493 #[validate(custom(function = validate::osd_timeout))]
494 #[merge(strategy = merg::option::overwrite_some)]
495 osd_timeout: Option<Duration>,
496
497 #[serde(with = "mister_bool")]
501 #[merge(strategy = merg::option::overwrite_some)]
502 gamepad_defaults: Option<bool>,
503
504 #[serde(with = "mister_bool")]
508 #[merge(strategy = merg::option::overwrite_some)]
509 recents: Option<bool>,
510
511 #[serde(with = "mister_hexa")]
517 #[merge(strategy = merg::option::overwrite_some)]
518 jamma_vid: Option<u16>,
519
520 #[serde(with = "mister_hexa")]
521 #[merge(strategy = merg::option::overwrite_some)]
522 jamma_pid: Option<u16>,
523
524 #[serde(with = "mister_hexa")]
527 #[merge(strategy = merg::option::overwrite_some)]
528 no_merge_vid: Option<u16>,
529
530 #[serde(with = "mister_hexa")]
531 #[merge(strategy = merg::option::overwrite_some)]
532 no_merge_pid: Option<u16>,
533
534 #[serde(with = "mister_hexa_seq")]
535 #[merge(strategy = merg::vec::append)]
536 no_merge_vidpid: Vec<u32>,
537
538 #[serde(with = "mister_hexa")]
540 #[merge(strategy = merg::option::overwrite_some)]
541 spinner_vid: Option<u16>,
542
543 #[serde(with = "mister_hexa")]
544 #[merge(strategy = merg::option::overwrite_some)]
545 spinner_pid: Option<u16>,
546
547 #[validate(range(min = -10000, max = 10000))]
549 #[merge(strategy = merg::option::overwrite_some)]
550 spinner_throttle: Option<i32>,
551
552 #[merge(strategy = merg::option::overwrite_some)]
553 spinner_axis: Option<u8>,
554
555 #[serde(with = "mister_bool")]
556 #[merge(strategy = merg::option::overwrite_some)]
557 sniper_mode: Option<bool>,
558
559 #[serde(with = "mister_bool")]
560 #[merge(strategy = merg::option::overwrite_some)]
561 browse_expand: Option<bool>,
562
563 #[serde(with = "mister_bool")]
565 #[merge(strategy = merg::option::overwrite_some)]
566 logo: Option<bool>,
567
568 #[serde(with = "mister_bool")]
569 #[merge(strategy = merg::option::overwrite_some)]
570 log_file_entry: Option<bool>,
571
572 #[merge(strategy = merg::option::overwrite_some)]
573 shmask_mode_default: Option<u8>,
574
575 #[serde_as(as = "Option<DurationMinutes>")]
579 #[merge(strategy = merg::option::overwrite_some)]
580 bt_auto_disconnect: Option<Duration>,
581
582 #[serde(with = "mister_bool")]
583 #[merge(strategy = merg::option::overwrite_some)]
584 bt_reset_before_pair: Option<bool>,
585
586 #[serde(alias = "video_mode")]
587 #[merge(strategy = merg::option::overwrite_some)]
588 video_conf: Option<String>,
589
590 #[serde(alias = "video_mode_pal")]
591 #[merge(strategy = merg::option::overwrite_some)]
592 video_conf_pal: Option<String>,
593
594 #[serde(alias = "video_mode_ntsc")]
595 #[merge(strategy = merg::option::overwrite_some)]
596 video_conf_ntsc: Option<String>,
597
598 #[merge(strategy = merg::option::overwrite_some)]
599 font: Option<String>,
600
601 #[merge(strategy = merg::option::overwrite_some)]
602 shared_folder: Option<String>,
603
604 #[merge(strategy = merg::option::overwrite_some)]
605 waitmount: Option<String>,
606
607 #[merge(strategy = merg::option::overwrite_some)]
608 afilter_default: Option<String>,
609
610 #[merge(strategy = merg::option::overwrite_some)]
611 vfilter_default: Option<String>,
612
613 #[merge(strategy = merg::option::overwrite_some)]
614 vfilter_vertical_default: Option<String>,
615
616 #[merge(strategy = merg::option::overwrite_some)]
617 vfilter_scanlines_default: Option<String>,
618
619 #[merge(strategy = merg::option::overwrite_some)]
620 shmask_default: Option<String>,
621
622 #[merge(strategy = merg::option::overwrite_some)]
623 preset_default: Option<String>,
624
625 #[serde(default)]
626 #[merge(strategy = merg::vec::append)]
627 player_controller: Vec<Vec<String>>,
628
629 #[serde(default)]
630 #[merge(strategy = merg::vec::append)]
631 player_1_controller: Vec<String>,
632 #[serde(default)]
633 #[merge(strategy = merg::vec::append)]
634 player_2_controller: Vec<String>,
635 #[serde(default)]
636 #[merge(strategy = merg::vec::append)]
637 player_3_controller: Vec<String>,
638 #[serde(default)]
639 #[merge(strategy = merg::vec::append)]
640 player_4_controller: Vec<String>,
641 #[serde(default)]
642 #[merge(strategy = merg::vec::append)]
643 player_5_controller: Vec<String>,
644 #[serde(default)]
645 #[merge(strategy = merg::vec::append)]
646 player_6_controller: Vec<String>,
647
648 #[serde(with = "mister_bool")]
649 #[merge(strategy = merg::option::overwrite_some)]
650 rumble: Option<bool>,
651
652 #[merge(strategy = merg::option::overwrite_some)]
653 #[validate(range(min = 0, max = 100))]
654 wheel_force: Option<u8>,
655
656 #[merge(strategy = merg::option::overwrite_some)]
657 #[validate(range(min = 0, max = 1000))]
658 wheel_range: Option<u16>,
659
660 #[serde(with = "mister_bool")]
661 #[merge(strategy = merg::option::overwrite_some)]
662 hdmi_game_mode: Option<bool>,
663
664 #[merge(strategy = merg::option::overwrite_some)]
670 vrr_mode: Option<VrrModeConfig>,
671
672 #[merge(strategy = merg::option::overwrite_some)]
673 vrr_min_framerate: Option<u8>,
674
675 #[merge(strategy = merg::option::overwrite_some)]
676 vrr_max_framerate: Option<u8>,
677
678 #[merge(strategy = merg::option::overwrite_some)]
679 vrr_vesa_framerate: Option<u8>,
680
681 #[serde_as(as = "Option<DurationSeconds<u64>>")]
683 #[merge(strategy = merg::option::overwrite_some)]
684 #[validate(custom(function = validate::video_off))]
685 video_off: Option<Duration>,
686
687 #[serde(with = "mister_bool")]
688 #[merge(strategy = merg::option::overwrite_some)]
689 disable_autofire: Option<bool>,
690
691 #[merge(strategy = merg::option::overwrite_some)]
692 #[validate(range(min = 0, max = 100))]
693 video_brightness: Option<u8>,
694
695 #[merge(strategy = merg::option::overwrite_some)]
696 #[validate(range(min = 0, max = 100))]
697 video_contrast: Option<u8>,
698
699 #[merge(strategy = merg::option::overwrite_some)]
700 #[validate(range(min = 0, max = 100))]
701 video_saturation: Option<u8>,
702
703 #[merge(strategy = merg::option::overwrite_some)]
704 #[validate(range(min = 0, max = 360))]
705 video_hue: Option<u16>,
706
707 #[merge(strategy = merg::option::overwrite_some)]
708 video_gain_offset: Option<VideoGainOffsets>,
709
710 #[merge(strategy = merg::option::overwrite_some)]
711 hdr: Option<HdrConfig>,
712
713 #[merge(strategy = merg::option::overwrite_some)]
714 #[validate(range(min = 100, max = 10000))]
715 hdr_max_nits: Option<u16>,
716
717 #[merge(strategy = merg::option::overwrite_some)]
718 #[validate(range(min = 100, max = 10000))]
719 hdr_avg_nits: Option<u16>,
720
721 #[serde(with = "mister_hexa_seq")]
722 #[merge(strategy = merg::vec::append)]
723 controller_unique_mapping: Vec<u32>,
724}
725
726impl MisterConfig {
727 pub fn new() -> Self {
728 Default::default()
729 }
730
731 pub fn new_defaults() -> Self {
732 let mut config = Self::new();
733 config.set_defaults();
734 config
735 }
736
737 pub fn set_defaults(&mut self) {
739 self.bootscreen.get_or_insert(true);
740 self.fb_terminal.get_or_insert(true);
741 self.controller_info.get_or_insert(Duration::from_secs(6));
742 self.browse_expand.get_or_insert(true);
743 self.logo.get_or_insert(true);
744 self.rumble.get_or_insert(true);
745 self.wheel_force.get_or_insert(50);
746 self.hdr.get_or_insert(HdrConfig::default());
747 self.hdr_avg_nits.get_or_insert(250);
748 self.hdr_max_nits.get_or_insert(1000);
749 self.video_brightness.get_or_insert(50);
750 self.video_contrast.get_or_insert(50);
751 self.video_saturation.get_or_insert(100);
752 self.video_hue.get_or_insert(0);
753 self.video_gain_offset
754 .get_or_insert("1, 0, 1, 0, 1, 0".parse().unwrap());
755 self.video_conf.get_or_insert("6".to_string());
756 }
757
758 pub fn custom_aspect_ratio(&self) -> Vec<AspectRatio> {
759 if self.custom_aspect_ratio.is_empty() {
760 self.custom_aspect_ratio_1
761 .iter()
762 .chain(self.custom_aspect_ratio_2.iter())
763 .copied()
764 .collect()
765 } else {
766 self.custom_aspect_ratio.clone()
767 }
768 }
769
770 pub fn player_controller(&self) -> Vec<Vec<String>> {
771 if self.player_controller.is_empty() {
772 let mut vec = Vec::new();
773 if !self.player_1_controller.is_empty() {
774 vec.push(self.player_1_controller.clone());
775 }
776 if !self.player_2_controller.is_empty() {
777 vec.push(self.player_2_controller.clone());
778 }
779 if !self.player_3_controller.is_empty() {
780 vec.push(self.player_3_controller.clone());
781 }
782 if !self.player_4_controller.is_empty() {
783 vec.push(self.player_4_controller.clone());
784 }
785 if !self.player_5_controller.is_empty() {
786 vec.push(self.player_5_controller.clone());
787 }
788 if !self.player_6_controller.is_empty() {
789 vec.push(self.player_6_controller.clone());
790 }
791
792 vec
793 } else {
794 self.player_controller.clone()
795 }
796 }
797
798 #[inline]
799 pub fn hdmi_limited(&self) -> HdmiLimitedConfig {
800 self.hdmi_limited.unwrap_or_default()
801 }
802
803 #[inline]
804 pub fn hdmi_game_mode(&self) -> bool {
805 self.hdmi_game_mode.unwrap_or_default()
806 }
807
808 #[inline]
809 pub fn hdr(&self) -> HdrConfig {
810 self.hdr.unwrap_or_default()
811 }
812
813 #[inline]
814 pub fn hdr_max_nits(&self) -> u16 {
815 self.hdr_max_nits.unwrap_or(1000)
816 }
817
818 #[inline]
819 pub fn hdr_avg_nits(&self) -> u16 {
820 self.hdr_avg_nits.unwrap_or(250)
821 }
822
823 #[inline]
824 pub fn dvi_mode(&self) -> bool {
825 self.dvi_mode.unwrap_or_default()
826 }
827
828 #[inline]
829 pub fn dvi_mode_raw(&self) -> Option<bool> {
830 self.dvi_mode
831 }
832
833 #[inline]
834 pub fn hdmi_audio_96k(&self) -> bool {
835 self.hdmi_audio_96k.unwrap_or_default()
836 }
837
838 #[inline]
840 pub fn video_brightness(&self) -> f32 {
841 (self.video_brightness.unwrap_or(50).clamp(0, 100) as f32 / 100.0) - 0.5
842 }
843
844 #[inline]
846 pub fn video_contrast(&self) -> f32 {
847 ((self.video_contrast.unwrap_or(50).clamp(0, 100) as f32 / 100.0) - 0.5) * 2. + 1.
848 }
849
850 #[inline]
852 pub fn video_saturation(&self) -> f32 {
853 self.video_saturation.unwrap_or(100).clamp(0, 100) as f32 / 100.
854 }
855
856 #[inline]
858 pub fn video_hue_radian(&self) -> f32 {
859 (self.video_hue.unwrap_or_default() as f32) * f32::PI() / 180.
860 }
861
862 #[inline]
864 pub fn video_gain_offset(&self) -> VideoGainOffsets {
865 self.video_gain_offset.unwrap_or_default()
866 }
867
868 #[inline]
870 pub fn vga_mode(&self) -> VgaMode {
871 self.vga_mode.unwrap_or_default()
872 }
873
874 #[inline]
876 pub fn direct_video(&self) -> bool {
877 self.direct_video.unwrap_or_default()
878 }
879
880 #[inline]
882 pub fn vsync_adjust(&self) -> VsyncAdjustConfig {
883 if self.direct_video() {
884 VsyncAdjustConfig::Disabled
885 } else {
886 self.vsync_adjust.unwrap_or_default()
887 }
888 }
889
890 #[inline]
892 pub fn menu_pal(&self) -> bool {
893 self.menu_pal.unwrap_or_default()
894 }
895
896 #[inline]
898 pub fn forced_scandoubler(&self) -> bool {
899 self.forced_scandoubler.unwrap_or_default()
900 }
901}
902
903#[cfg(test)]
904pub mod testing {
905 use std::path::PathBuf;
906
907 pub(super) static mut ROOT: Option<PathBuf> = None;
908
909 #[cfg(test)]
910 pub fn set_config_root(root: impl Into<PathBuf>) {
911 unsafe {
912 ROOT = Some(root.into());
913 }
914 }
915}
916
917#[derive(Default, Debug, Clone, Deserialize, Merge)]
918#[serde(default)]
919pub struct Config {
920 #[serde(rename = "MiSTer")]
922 mister: MisterConfig,
923
924 #[serde(flatten)]
926 #[merge(strategy = merg::hashmap::recurse)]
927 overrides: HashMap<String, MisterConfig>,
928}
929
930impl Config {
931 fn root() -> PathBuf {
932 #[cfg(test)]
933 {
934 unsafe { testing::ROOT.clone().unwrap() }
935 }
936
937 #[cfg(not(test))]
938 PathBuf::from("/media/fat")
939 }
940
941 pub fn into_inner(self) -> MisterConfig {
942 self.mister
943 }
944
945 pub fn into_inner_with_overrides(self, overrides: &[&str]) -> MisterConfig {
946 let mut mister = self.mister;
947 for o in overrides {
948 if let Some(override_config) = self.overrides.get(&o.to_string()) {
949 mister.merge(override_config.clone());
950 }
951 }
952 mister
953 }
954
955 pub fn base() -> Self {
956 let path = Self::root().join("MiSTer.ini");
957 Self::load(&path).unwrap_or_else(|_| {
958 info!(?path, "Failed to load MiSTer.ini, using defaults.");
959 let mut c = Self::default();
960 c.mister.set_defaults();
961 c
962 })
963 }
964
965 pub fn cores_root() -> PathBuf {
966 Self::root()
967 }
968
969 pub fn config_root() -> PathBuf {
970 Self::root().join("config")
971 }
972
973 pub fn last_core_data() -> Option<String> {
974 std::fs::read_to_string(Self::config_root().join("lastcore.dat")).ok()
975 }
976
977 pub fn merge_core_override(&mut self, corename: &str) {
978 if let Some(o) = self.overrides.get(corename) {
979 self.mister.merge(o.clone());
980 }
981 }
982
983 pub fn merge_video_override(&mut self, resolution: Resolution) {
984 let video_str = format!("video={}", resolution);
986 if let Some(o) = self.overrides.get(&video_str) {
987 self.mister.merge(o.clone());
988 }
989 }
990
991 pub fn from_ini<R: io::Read>(mut content: R) -> Result<Self, ConfigError> {
995 let mut s = String::new();
996 content.read_to_string(&mut s)?;
997
998 if s.is_empty() {
999 return Ok(Default::default());
1000 }
1001
1002 let json = ini::parse(&s).unwrap().to_json_string(
1003 |name, value: &str| match name {
1004 "mouse_throttle"
1005 | "video_info"
1006 | "controller_info"
1007 | "refresh_min"
1008 | "refresh_max"
1009 | "vscale_border"
1010 | "bootcore_timeout"
1011 | "osd_timeout"
1012 | "spinner_throttle"
1013 | "spinner_axis"
1014 | "shmask_mode_default"
1015 | "bt_auto_disconnect"
1016 | "wheel_force"
1017 | "wheel_range"
1018 | "vrr_min_framerate"
1019 | "vrr_max_framerate"
1020 | "vrr_vesa_framerate"
1021 | "video_off"
1022 | "video_brightness"
1023 | "video_contrast"
1024 | "video_saturation"
1025 | "video_hue"
1026 | "hdr_max_nits"
1027 | "hdr_avg_nits" => Some(value.to_string()),
1028 _ => None,
1029 },
1030 |name| {
1031 [
1032 "custom_aspect_ratio",
1033 "no_merge_vidpid",
1034 "player_controller",
1035 "player_1_controller",
1036 "player_2_controller",
1037 "player_3_controller",
1038 "player_4_controller",
1039 "player_5_controller",
1040 "player_6_controller",
1041 "controller_unique_mapping",
1042 ]
1043 .contains(&name)
1044 },
1045 |name: &str| -> Option<&str> {
1046 if name == "ypbpr" {
1047 Some("vga_mode")
1048 } else {
1049 None
1050 }
1051 },
1052 );
1053
1054 Config::from_json(json.as_bytes())
1055 }
1056
1057 pub fn from_json<R: io::Read>(mut content: R) -> Result<Self, ConfigError> {
1058 let mut c = String::new();
1059 content.read_to_string(&mut c)?;
1060 Ok(json5::from_str(&c)?)
1061 }
1062
1063 pub fn load(path: impl AsRef<Path>) -> Result<Self, ConfigError> {
1064 let path = path.as_ref();
1065 match path.extension().and_then(|ext| ext.to_str()) {
1066 Some("ini") => Self::from_ini(std::fs::File::open(path)?),
1067 Some("json") => Self::from_json(std::fs::File::open(path)?),
1068 _ => Err(
1069 io::Error::new(io::ErrorKind::InvalidInput, "Invalid config file extension").into(),
1070 ),
1071 }
1072 }
1073
1074 pub unsafe fn copy_to_cfg_cpp(self, dest: &mut cpp::CppCfg) {
1079 unsafe fn copy_string<const N: usize>(dest: &mut [c_char; N], src: &str) {
1080 let src = src.as_bytes();
1081 let l = src.len().min(N);
1082 for i in 0..l {
1083 let c = src[i] as c_char;
1084 dest[i] = c;
1085 }
1086 dest[l] = 0;
1087 }
1088
1089 let m = &self.mister;
1090
1091 dest.keyrah_mode = m.keyrah_mode.unwrap_or_default();
1092 dest.forced_scandoubler = m.forced_scandoubler.unwrap_or_default() as u8;
1093 dest.key_menu_as_rgui = m.key_menu_as_rgui.unwrap_or_default() as u8;
1094 dest.reset_combo = m.reset_combo.unwrap_or_default() as u8;
1095 dest.csync = m.composite_sync.unwrap_or_default() as u8;
1096 dest.vga_scaler = m.vga_scaler.unwrap_or_default() as u8;
1097 dest.vga_sog = m.vga_sog.unwrap_or_default() as u8;
1098 dest.hdmi_audio_96k = m.hdmi_audio_96k.unwrap_or_default() as u8;
1099 dest.dvi_mode = m.dvi_mode.map(|x| x as u8).unwrap_or(2);
1101 dest.hdmi_limited = m.hdmi_limited.unwrap_or_default() as u8;
1102 dest.direct_video = m.direct_video.unwrap_or_default() as u8;
1103 dest.video_info = m.video_info.unwrap_or_default().as_secs() as u8;
1104 dest.refresh_min = m.refresh_min.unwrap_or_default();
1105 dest.refresh_max = m.refresh_max.unwrap_or_default();
1106 dest.controller_info = m.controller_info.unwrap_or_default().as_secs() as u8;
1107 dest.vsync_adjust = m.vsync_adjust.unwrap_or_default() as u8;
1108 dest.kbd_nomouse = m.kbd_nomouse.unwrap_or_default() as u8;
1109 dest.mouse_throttle = m.mouse_throttle.unwrap_or_default();
1110 dest.bootscreen = m.bootscreen.unwrap_or_default() as u8;
1111 dest.vscale_mode = m.vscale_mode.unwrap_or_default() as u8;
1112 dest.vscale_border = m.vscale_border.unwrap_or_default();
1113 dest.rbf_hide_datecode = m.rbf_hide_datecode.unwrap_or_default() as u8;
1114 dest.menu_pal = m.menu_pal.unwrap_or_default() as u8;
1115 dest.bootcore_timeout = m.bootcore_timeout.unwrap_or_default().as_secs() as i16;
1116 dest.fb_size = m.fb_size.unwrap_or_default() as u8;
1117 dest.fb_terminal = m.fb_terminal.unwrap_or_default() as u8;
1118 dest.osd_rotate = m.osd_rotate.unwrap_or_default() as u8;
1119 dest.osd_timeout = m.osd_timeout.unwrap_or_default().as_secs() as u16;
1120 dest.gamepad_defaults = m.gamepad_defaults.unwrap_or_default() as u8;
1121 dest.recents = m.recents.unwrap_or_default() as u8;
1122 dest.jamma_vid = m.jamma_vid.unwrap_or_default();
1123 dest.jamma_pid = m.jamma_pid.unwrap_or_default();
1124 dest.no_merge_vid = m.no_merge_vid.unwrap_or_default();
1125 dest.no_merge_pid = m.no_merge_pid.unwrap_or_default();
1126 dest.no_merge_vidpid = [0; 256];
1127 for (i, vidpid) in m.no_merge_vidpid.iter().enumerate() {
1128 dest.no_merge_vidpid[i] = *vidpid;
1129 }
1130 dest.spinner_vid = m.spinner_vid.unwrap_or_default();
1131 dest.spinner_pid = m.spinner_pid.unwrap_or_default();
1132 dest.spinner_throttle = m.spinner_throttle.unwrap_or_default() as c_int;
1133 dest.spinner_axis = m.spinner_axis.unwrap_or_default();
1134 dest.sniper_mode = m.sniper_mode.unwrap_or_default() as u8;
1135 dest.browse_expand = m.browse_expand.unwrap_or_default() as u8;
1136 dest.logo = m.logo.unwrap_or_default() as u8;
1137 dest.log_file_entry = m.log_file_entry.unwrap_or_default() as u8;
1138 dest.shmask_mode_default = m.shmask_mode_default.unwrap_or_default();
1139 dest.bt_auto_disconnect =
1140 (m.bt_auto_disconnect.unwrap_or_default().as_secs() / 60) as c_int;
1141 dest.bt_reset_before_pair = m.bt_reset_before_pair.unwrap_or_default() as c_int;
1142 copy_string(
1143 &mut dest.bootcore,
1144 &m.bootcore.clone().unwrap_or_default().to_string(),
1145 );
1146 copy_string(
1147 &mut dest.video_conf,
1148 &m.video_conf.clone().unwrap_or_default(),
1149 );
1150 copy_string(
1151 &mut dest.video_conf_pal,
1152 &m.video_conf_pal.clone().unwrap_or_default(),
1153 );
1154 copy_string(
1155 &mut dest.video_conf_ntsc,
1156 &m.video_conf_ntsc.clone().unwrap_or_default(),
1157 );
1158 copy_string(&mut dest.font, &m.font.clone().unwrap_or_default());
1159 copy_string(
1160 &mut dest.shared_folder,
1161 &m.shared_folder.clone().unwrap_or_default(),
1162 );
1163 copy_string(
1164 &mut dest.waitmount,
1165 &m.waitmount.clone().unwrap_or_default(),
1166 );
1167 let ar = m.custom_aspect_ratio();
1168 let aspect_ratio_1 = ar.first();
1169 let aspect_ratio_2 = ar.get(1);
1170 if let Some(a1) = aspect_ratio_1 {
1171 copy_string(&mut dest.custom_aspect_ratio[0], &a1.to_string());
1172 }
1173 if let Some(a2) = aspect_ratio_2 {
1174 copy_string(&mut dest.custom_aspect_ratio[1], &a2.to_string());
1175 }
1176 copy_string(
1177 &mut dest.afilter_default,
1178 &m.afilter_default.clone().unwrap_or_default(),
1179 );
1180 copy_string(
1181 &mut dest.vfilter_default,
1182 &m.vfilter_default.clone().unwrap_or_default(),
1183 );
1184 copy_string(
1185 &mut dest.vfilter_vertical_default,
1186 &m.vfilter_vertical_default.clone().unwrap_or_default(),
1187 );
1188 copy_string(
1189 &mut dest.vfilter_scanlines_default,
1190 &m.vfilter_scanlines_default.clone().unwrap_or_default(),
1191 );
1192 copy_string(
1193 &mut dest.shmask_default,
1194 &m.shmask_default.clone().unwrap_or_default(),
1195 );
1196 copy_string(
1197 &mut dest.preset_default,
1198 &m.preset_default.clone().unwrap_or_default(),
1199 );
1200 let player_controller = m.player_controller();
1201 for (i, pc) in player_controller.iter().enumerate().take(6) {
1202 for (j, p) in pc.iter().enumerate() {
1203 copy_string(&mut dest.player_controller[i][j], p);
1204 }
1205 }
1206 dest.rumble = m.rumble.unwrap_or_default() as u8;
1207 dest.wheel_force = m.wheel_force.unwrap_or_default();
1208 dest.wheel_range = m.wheel_range.unwrap_or_default();
1209 dest.hdmi_game_mode = m.hdmi_game_mode.unwrap_or_default() as u8;
1210 dest.vrr_mode = m.vrr_mode.unwrap_or_default() as u8;
1211 dest.vrr_min_framerate = m.vrr_min_framerate.unwrap_or_default();
1212 dest.vrr_max_framerate = m.vrr_max_framerate.unwrap_or_default();
1213 dest.vrr_vesa_framerate = m.vrr_vesa_framerate.unwrap_or_default();
1214 dest.video_off = m.video_off.unwrap_or_default().as_secs() as u16;
1215 dest.disable_autofire = m.disable_autofire.unwrap_or_default() as u8;
1216 dest.video_brightness = m.video_brightness.unwrap_or_default();
1217 dest.video_contrast = m.video_contrast.unwrap_or_default();
1218 dest.video_saturation = m.video_saturation.unwrap_or_default();
1219 dest.video_hue = m.video_hue.unwrap_or_default();
1220 copy_string(
1221 &mut dest.video_gain_offset,
1222 &match m.video_gain_offset.as_ref() {
1223 Some(v) => v.to_string(),
1224 None => "1, 0, 1, 0, 1, 0".to_string(),
1225 },
1226 );
1227 dest.hdr = m.hdr.unwrap_or_default() as u8;
1228 dest.hdr_max_nits = m.hdr_max_nits.unwrap_or_default();
1229 dest.hdr_avg_nits = m.hdr_avg_nits.unwrap_or_default();
1230 copy_string(
1231 &mut dest.vga_mode,
1232 &m.vga_mode.unwrap_or_default().to_string(),
1233 );
1234 dest.vga_mode_int = m.vga_mode.unwrap_or_default() as u8 as c_char;
1235 dest.ntsc_mode = m.ntsc_mode.unwrap_or_default() as u8 as c_char;
1236 for (i, mapping) in m.controller_unique_mapping.iter().enumerate().take(256) {
1237 dest.controller_unique_mapping[i] = *mapping;
1238 }
1239 }
1240
1241 pub fn merge(&mut self, other: Config) {
1243 Merge::merge(self, other);
1244 }
1245}
1246
1247#[test]
1248fn works_with_empty_file() {
1249 unsafe {
1250 let mut cpp_cfg: cpp::CppCfg = std::mem::zeroed();
1251 let config = Config::from_ini(io::empty()).unwrap();
1252 config.copy_to_cfg_cpp(&mut cpp_cfg);
1253 }
1254}
1255
1256#[cfg(test)]
1257mod examples {
1258 use crate::config::*;
1259
1260 #[rstest::rstest]
1261 fn works_with_example(#[files("tests/assets/config/*.ini")] p: PathBuf) {
1262 unsafe {
1263 let mut cpp_cfg: cpp::CppCfg = std::mem::zeroed();
1264 let config = Config::load(p).unwrap();
1265 config.copy_to_cfg_cpp(&mut cpp_cfg);
1266 }
1267 }
1268}