mister_fpga/
config_string.rs

1//! Parses the config string of a core, and extract the proper parameters.
2//! See this documentation for more information:
3//! https://github.com/MiSTer-devel/MkDocs_MiSTer/blob/main/docs/developer/conf_str.md
4//!
5//! This is located in utils to allow to run test. There is no FPGA or MiSTer specific
6//! code in this module, even though it isn't used outside of MiSTer itself.
7
8use std::collections::HashMap;
9use std::convert::TryFrom;
10use std::fmt::{Debug, Formatter};
11use std::ops::{Deref, Range};
12use std::path::Path;
13use std::str::FromStr;
14
15use once_cell::sync::Lazy;
16use regex::Regex;
17use tracing::{debug, warn};
18
19use one_fpga::core::{CoreSettingItem, CoreSettings, SettingId};
20pub use types::*;
21
22use crate::fpga::user_io;
23use crate::types::StatusBitMap;
24
25pub mod midi;
26pub mod settings;
27pub mod uart;
28
29mod parser;
30
31mod types;
32
33static LABELED_SPEED_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"(\d*)(\([^)]*\))?").unwrap());
34
35#[derive(Clone, Copy, PartialEq, Eq)]
36pub struct FileExtension(pub [u8; 3]);
37
38impl Debug for FileExtension {
39    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
40        f.debug_tuple("FileExtension")
41            .field(&format_args!("'{}'", self.as_str()))
42            .finish()
43    }
44}
45
46impl Deref for FileExtension {
47    type Target = str;
48
49    fn deref(&self) -> &Self::Target {
50        unsafe { std::str::from_utf8_unchecked(&self.0).trim() }
51    }
52}
53
54impl FromStr for FileExtension {
55    type Err = &'static str;
56
57    fn from_str(s: &str) -> Result<Self, Self::Err> {
58        if s.len() > 3 {
59            return Err("Invalid file extension");
60        }
61
62        let bytes = s.as_bytes();
63        Ok(Self([
64            bytes.first().copied().unwrap_or(b' '),
65            bytes.get(1).copied().unwrap_or(b' '),
66            bytes.get(2).copied().unwrap_or(b' '),
67        ]))
68    }
69}
70
71impl FileExtension {
72    pub fn as_str(&self) -> &str {
73        self.deref()
74    }
75}
76
77#[derive(Debug, Clone)]
78pub struct LoadFileInfo {
79    /// Core supports save files, load a file, and mount a save for reading or writing
80    pub save_support: bool,
81
82    /// Explicit index (or index is generated from line number if index not given).
83    pub index: u8,
84
85    /// A concatenated list of 3 character file extensions. For example, BINGEN would be
86    /// BIN and GEN extensions.
87    pub extensions: Vec<FileExtension>,
88
89    /// Optional {Text} string is the text that is displayed before the extensions like
90    /// "Load RAM". If {Text} is not specified, then default is "Load *".
91    pub label: Option<String>,
92
93    /// Optional {Address} - load file directly into DDRAM at this address.
94    /// ioctl_index from hps_io will be: ioctl_index[5:0] = index(explicit or auto),
95    /// ioctl_index[7:6] = extension index
96    pub address: Option<FpgaRamMemoryAddress>,
97}
98
99impl LoadFileInfo {
100    pub fn setting_id(&self) -> SettingId {
101        self.label.as_ref().map_or_else(
102            || SettingId::new(self.index as u32),
103            |l| SettingId::from_label(&l),
104        )
105    }
106}
107
108/// A component of a Core config string.
109#[derive(Debug, Clone)]
110pub enum ConfigMenu {
111    /// Empty lines, potentially with text.
112    Empty(Option<String>),
113
114    /// Cheat menu option.
115    Cheat(Option<String>),
116
117    /// Disable the option if the menu mask is set.
118    DisableIf(u32, Box<ConfigMenu>),
119
120    /// Disable the option if the menu mask is NOT set.
121    DisableUnless(u32, Box<ConfigMenu>),
122
123    /// Hide the option if the menu mask is set.
124    HideIf(u32, Box<ConfigMenu>),
125
126    /// Hide the option if the menu mask is NOT set.
127    HideUnless(u32, Box<ConfigMenu>),
128
129    /// DIP switch menu option.
130    Dip,
131
132    /// Load file menu option.
133    LoadFile(Box<LoadFileInfo>),
134
135    /// Open file and remember it, useful for remembering an alternative rom, config, or other
136    /// type of file. See [Self::LoadFile] for more information.
137    LoadFileAndRemember(Box<LoadFileInfo>),
138
139    /// Mount SD card menu option.
140    MountSdCard {
141        slot: u8,
142        extensions: Vec<FileExtension>,
143        label: Option<String>,
144    },
145
146    /// `O{Index1}[{Index2}],{Name},{Options...}` - Option button that allows you to select
147    /// between various choices.
148    ///
149    /// - `{Index1}` and `{Index2}` are values from 0-9 and A-V (like Hex, but it extends
150    ///   from A-V instead of A-F). This represents all 31 bits. First and second index
151    ///   are the range of bits that will be set in the status register.
152    /// - `{Name}` is what is shown to describe the option.
153    /// - {Options...} - a list of comma separated options.
154    Option {
155        bits: Range<u8>,
156        label: String,
157        choices: Vec<String>,
158    },
159
160    /// `T{Index},{Name}` - Trigger button. This is a simple button that will pulse HIGH of
161    /// specified `{Index}` bit in status register. A perfect example of this is for a reset
162    /// button. `{Name}` is the text that describes the button function.
163    /// `R{Index},{Name}` is the same but should close the OSD.
164    Trigger {
165        close_osd: bool,
166        index: u8,
167        label: String,
168    },
169
170    /// `J[1],{Button1}[,{Button2},...]` - J1 means lock keyboard to joystick emulation mode.
171    /// Useful for keyboard-less systems such as consoles. `{Button1},{Button2},...` is list
172    /// of joystick buttons used in the core. Up to 12 buttons can be listed. Analog axis
173    /// are not defined here. The user just needs to map them through the Menu core.
174    JoystickButtons {
175        /// If true, the keyboard should be locked to joystick emulation mode.
176        keyboard: bool,
177
178        /// List of buttons that can be mapped.
179        buttons: Vec<String>,
180    },
181
182    SnesButtonDefaultList {
183        buttons: Vec<String>,
184    },
185
186    SnesButtonDefaultPositionalList {
187        buttons: Vec<String>,
188    },
189
190    /// `P{#},{Title}` - Creates sub-page for options with `{Title}`.
191    /// `P{#}` - Prefix to place the option into specific `{#}` page. This is added
192    ///          before O# but after something like d#. (e.g.
193    ///          "d5P1o2,Vertical Crop,Disabled,216p(5x);", is correct and
194    ///          "P1d5o2,Vertical Crop,Disabled,216p(5x);", is incorrect and the menu
195    ///          options will not work).
196    Page {
197        /// The page number.
198        index: u8,
199
200        /// The title of the page.
201        label: String,
202    },
203
204    /// A page item, which is a sub-menu. The first item is the page
205    /// index, the second is the item.
206    PageItem(u8, Box<ConfigMenu>),
207
208    /// `I,INFO1,INFO2,...,INFO255` - INFO1-INFO255 lines to display as OSD info (top left
209    /// corner of screen).
210    Info(Vec<String>),
211
212    /// `V,{Version String}` - Version string. {Version String} is the version string.
213    /// Takes the core name and appends version string for name to display.
214    Version(String),
215}
216
217impl ConfigMenu {
218    pub fn as_option(&self) -> Option<&Self> {
219        match self {
220            ConfigMenu::Option { .. } => Some(self),
221            ConfigMenu::DisableIf(_, sub)
222            | ConfigMenu::DisableUnless(_, sub)
223            | ConfigMenu::HideIf(_, sub)
224            | ConfigMenu::HideUnless(_, sub)
225            | ConfigMenu::PageItem(_, sub) => sub.as_option(),
226            _ => None,
227        }
228    }
229
230    pub fn as_trigger(&self) -> Option<&Self> {
231        match self {
232            ConfigMenu::Trigger { .. } => Some(self),
233            ConfigMenu::DisableIf(_, sub)
234            | ConfigMenu::DisableUnless(_, sub)
235            | ConfigMenu::HideIf(_, sub)
236            | ConfigMenu::HideUnless(_, sub)
237            | ConfigMenu::PageItem(_, sub) => sub.as_trigger(),
238            _ => None,
239        }
240    }
241
242    pub fn as_core_menu_item(&self, status: &StatusBitMap) -> Vec<CoreSettingItem> {
243        match self {
244            ConfigMenu::LoadFile(info) | ConfigMenu::LoadFileAndRemember(info) => {
245                vec![CoreSettingItem::file_select(
246                    info.setting_id(),
247                    info.label
248                        .as_ref()
249                        .map_or_else(|| "Load File", String::as_str),
250                    info.extensions.iter().map(|e| e.to_string()).collect(),
251                )]
252            }
253            ConfigMenu::Option {
254                label,
255                choices,
256                bits,
257            } => {
258                let value = status.get_range(bits.clone());
259                match choices.len() {
260                    0 => vec![],
261                    1 => vec![CoreSettingItem::trigger(label, label)],
262                    2 => vec![CoreSettingItem::bool_option(label, label, Some(value != 0))],
263                    _ => vec![CoreSettingItem::int_option(
264                        label,
265                        label,
266                        choices.clone(),
267                        Some(value as usize % choices.len()),
268                    )],
269                }
270            }
271            ConfigMenu::Trigger { label, .. } => vec![CoreSettingItem::trigger(label, label)],
272            ConfigMenu::Page { label, .. } => {
273                vec![CoreSettingItem::page(label, label, label, Vec::new())]
274            }
275            ConfigMenu::PageItem(_, sub) => sub.as_core_menu_item(status),
276            ConfigMenu::HideIf(mask, sub) => {
277                if status.get(*mask as usize) {
278                    vec![]
279                } else {
280                    sub.as_core_menu_item(status)
281                }
282            }
283            ConfigMenu::HideUnless(mask, sub) => {
284                if !status.get(*mask as usize) {
285                    vec![]
286                } else {
287                    sub.as_core_menu_item(status)
288                }
289            }
290            ConfigMenu::DisableIf(mask, sub) => sub
291                .as_core_menu_item(status)
292                .into_iter()
293                .map(|item| item.with_disabled(status.get(*mask as usize)))
294                .collect(),
295            ConfigMenu::DisableUnless(mask, sub) => sub
296                .as_core_menu_item(status)
297                .into_iter()
298                .map(|item| item.with_disabled(!status.get(*mask as usize)))
299                .collect(),
300            ConfigMenu::Empty(label) => {
301                if let Some(label) = label {
302                    vec![CoreSettingItem::label(false, label)]
303                } else {
304                    vec![CoreSettingItem::Separator]
305                }
306            }
307            ConfigMenu::Info(_) => vec![],
308            ConfigMenu::Version(v) => {
309                vec![CoreSettingItem::label(false, &format!("Version: {}", v))]
310            }
311            _ => vec![],
312        }
313    }
314
315    pub fn as_load_file(&self) -> Option<&Self> {
316        match self {
317            ConfigMenu::LoadFile(_) | ConfigMenu::LoadFileAndRemember(_) => Some(self),
318            ConfigMenu::DisableIf(_, sub)
319            | ConfigMenu::DisableUnless(_, sub)
320            | ConfigMenu::HideIf(_, sub)
321            | ConfigMenu::HideUnless(_, sub)
322            | ConfigMenu::PageItem(_, sub) => sub.as_load_file(),
323            _ => None,
324        }
325    }
326
327    pub fn as_load_file_info(&self) -> Option<&LoadFileInfo> {
328        match self {
329            ConfigMenu::LoadFile(info) | ConfigMenu::LoadFileAndRemember(info) => Some(info),
330            _ => None,
331        }
332    }
333
334    pub fn setting_id(&self) -> Option<SettingId> {
335        match self {
336            ConfigMenu::Page { label, .. } => Some(SettingId::from_label(&label)),
337            ConfigMenu::Option { label, .. } => Some(SettingId::from_label(&label)),
338            ConfigMenu::Trigger { label, .. } => Some(SettingId::from_label(&label)),
339            ConfigMenu::PageItem(_, sub) => sub.setting_id(),
340            ConfigMenu::HideIf(_, sub)
341            | ConfigMenu::DisableIf(_, sub)
342            | ConfigMenu::HideUnless(_, sub)
343            | ConfigMenu::DisableUnless(_, sub) => sub.setting_id(),
344            _ => None,
345        }
346    }
347
348    pub fn label(&self) -> Option<&str> {
349        match self {
350            ConfigMenu::DisableIf(_, sub)
351            | ConfigMenu::DisableUnless(_, sub)
352            | ConfigMenu::HideIf(_, sub)
353            | ConfigMenu::HideUnless(_, sub) => sub.label(),
354            // TODO: add those.
355            // ConfigMenu::Cheat(name) => name.as_ref().map(|x| x.as_str()),
356            ConfigMenu::LoadFileAndRemember(info) | ConfigMenu::LoadFile(info) => {
357                info.label.as_ref().map(|l| l.as_str())
358            }
359            ConfigMenu::Option { label, .. } => Some(label.as_str()),
360            ConfigMenu::Trigger { label, .. } => Some(label.as_str()),
361            ConfigMenu::PageItem(_, sub) => sub.label(),
362            _ => None,
363        }
364    }
365
366    pub fn page(&self) -> Option<u8> {
367        match self {
368            ConfigMenu::DisableIf(_, inner) => inner.page(),
369            ConfigMenu::DisableUnless(_, inner) => inner.page(),
370            ConfigMenu::HideIf(_, inner) => inner.page(),
371            ConfigMenu::HideUnless(_, inner) => inner.page(),
372            ConfigMenu::PageItem(index, _) => Some(*index),
373            _ => None,
374        }
375    }
376}
377
378#[derive(Debug, Clone)]
379pub struct Config {
380    /// The name of the core.
381    pub name: String,
382
383    /// Unordered set of settings, e.g. save state memory and UART speed.
384    pub settings: settings::Settings,
385
386    /// Menu of the core.
387    pub menu: Vec<ConfigMenu>,
388}
389
390impl Config {
391    /// Create a new config from the FPGA.
392    /// This is disabled in Test as this module is still included in the test build.
393    pub fn from_fpga(fpga: &mut crate::fpga::MisterFpga) -> Result<Self, String> {
394        let mut cfg_string = String::with_capacity(1024);
395        fpga.spi_mut()
396            .execute(user_io::UserIoGetString(&mut cfg_string))?;
397        debug!(?cfg_string, "Config string from FPGA");
398
399        Self::from_str(&cfg_string)
400    }
401
402    pub fn settings(&self) -> &settings::Settings {
403        &self.settings
404    }
405
406    pub fn status_bit_map_mask(&self) -> StatusBitMap {
407        let mut arr = StatusBitMap::new();
408        // First bit is always 1 for soft reset (reserved).
409        arr.set(0, true);
410
411        for item in self.menu.iter() {
412            if let Some(ConfigMenu::Option { ref bits, .. }) = item.as_option() {
413                for i in bits.clone() {
414                    arr.set(i as usize, true);
415                }
416            } else if let Some(ConfigMenu::Trigger { index, .. }) = item.as_trigger() {
417                arr.set(*index as usize, true);
418            }
419        }
420
421        arr
422    }
423
424    pub fn load_info(&self, path: impl AsRef<Path>) -> Result<Option<LoadFileInfo>, String> {
425        let path_ext = match path.as_ref().extension() {
426            Some(ext) => ext.to_string_lossy(),
427            None => return Err("No extension".to_string()),
428        };
429
430        for item in self.menu.iter() {
431            if let ConfigMenu::LoadFile(ref info) = item {
432                if info
433                    .extensions
434                    .iter()
435                    .any(|ext| ext.eq_ignore_ascii_case(&path_ext))
436                {
437                    return Ok(Some(info.as_ref().clone()));
438                }
439            }
440        }
441        Ok(None)
442    }
443
444    pub fn snes_default_button_list(&self) -> Option<&Vec<String>> {
445        for item in self.menu.iter() {
446            if let ConfigMenu::SnesButtonDefaultList { ref buttons } = item {
447                return Some(buttons);
448            }
449        }
450        None
451    }
452
453    pub fn version(&self) -> Option<&str> {
454        for item in self.menu.iter() {
455            if let ConfigMenu::Version(ref version) = item {
456                return Some(version);
457            }
458        }
459        None
460    }
461
462    pub fn as_core_settings(&self, bits: &StatusBitMap) -> CoreSettings {
463        let it = self.menu.iter().flat_map(|item| {
464            item.as_core_menu_item(bits)
465                .into_iter()
466                .map(move |i| (item, i))
467        });
468
469        let mut root = Vec::new();
470        let mut pages: HashMap<u8, usize> = HashMap::new();
471        for (config_menu, core_menu) in it {
472            if let ConfigMenu::Page { index, .. } = config_menu {
473                pages.insert(*index, root.len());
474                root.push(core_menu);
475                continue;
476            }
477
478            // Find the page number.
479            match config_menu.page() {
480                None | Some(0) => root.push(core_menu),
481                Some(page) => {
482                    if let Some(i) = pages.get(&page) {
483                        let page = root.get_mut(*i).unwrap();
484                        page.add_item(core_menu);
485                    } else {
486                        warn!(?page, "Page hasn't been created yet");
487                    }
488                }
489            }
490        }
491
492        CoreSettings::new(self.name.clone(), root)
493    }
494}
495
496impl FromStr for Config {
497    type Err = String;
498
499    fn from_str(cfg_string: &str) -> Result<Self, Self::Err> {
500        let (rest, (name, settings, menu)) =
501            parser::parse_config_menu(cfg_string.into()).map_err(|e| e.to_string())?;
502
503        if !rest.fragment().is_empty() {
504            return Err(format!(
505                "Did not parse config string to the end. Rest: '{}'",
506                rest.fragment()
507            ));
508        }
509
510        Ok(Self {
511            name,
512            settings,
513            menu,
514        })
515    }
516}
517
518// Taken from https://github.com/MiSTer-devel/NES_MiSTer/blob/7a645f4/NES.sv#L219
519#[cfg(test)]
520const CONFIG_STRING_NES: &str = "\
521        NES;SS3E000000:200000,UART31250,MIDI;\
522        FS,NESFDSNSF;\
523        H1F2,BIN,Load FDS BIOS;\
524        -;\
525        ONO,System Type,NTSC,PAL,Dendy;\
526        -;\
527        C,Cheats;\
528        H2OK,Cheats Enabled,On,Off;\
529        -;\
530        oI,Autosave,On,Off;\
531        H5D0R6,Load Backup RAM;\
532        H5D0R7,Save Backup RAM;\
533        -;\
534        oC,Savestates to SDCard,On,Off;\
535        oDE,Savestate Slot,1,2,3,4;\
536        d7rA,Save state(Alt+F1-F4);\
537        d7rB,Restore state(F1-F4);\
538        -;\
539        P1,Audio & Video;\
540        P1-;\
541        P1oFH,Palette,Kitrinx,Smooth,Wavebeam,Sony CXA,PC-10 Better,Custom;\
542        H3P1FC3,PAL,Custom Palette;\
543        P1-;\
544        P1OIJ,Aspect ratio,Original,Full Screen,[ARC1],[ARC2];\
545        P1O13,Scandoubler Fx,None,HQ2x,CRT 25%,CRT 50%,CRT 75%;\
546        d6P1O5,Vertical Crop,Disabled,216p(5x);\
547        d6P1o36,Crop Offset,0,2,4,8,10,12,-12,-10,-8,-6,-4,-2;\
548        P1o78,Scale,Normal,V-Integer,Narrower HV-Integer,Wider HV-Integer;\
549        P1-;\
550        P1O4,Hide Overscan,Off,On;\
551        P1ORS,Mask Edges,Off,Left,Both,Auto;\
552        P1OP,Extra Sprites,Off,On;\
553        P1-;\
554        P1OUV,Audio Enable,Both,Internal,Cart Expansion,None;\
555        P2,Input Options;\
556        P2-;\
557        P2O9,Swap Joysticks,No,Yes;\
558        P2OA,Multitap,Disabled,Enabled;\
559        P2oJK,SNAC,Off,Controllers,Zapper,3D Glasses;\
560        P2o02,Periphery,None,Zapper(Mouse),Zapper(Joy1),Zapper(Joy2),Vaus,Vaus(A-Trigger),Powerpad,Family Trainer;\
561        P2oL,Famicom Keyboard,No,Yes;\
562        P2-;\
563        P2OL,Zapper Trigger,Mouse,Joystick;\
564        P2OM,Crosshairs,On,Off;\
565        P3,Miscellaneous;\
566        P3-;\
567        P3OG,Disk Swap,Auto,FDS button;\
568        P3o9,Pause when OSD is open,Off,On;\
569        - ;\
570        R0,Reset;\
571        J1,A,B,Select,Start,FDS,Mic,Zapper/Vaus Btn,PP/Mat 1,PP/Mat 2,PP/Mat 3,PP/Mat 4,PP/Mat 5,PP/Mat 6,PP/Mat 7,PP/Mat 8,PP/Mat 9,PP/Mat 10,PP/Mat 11,PP/Mat 12,Savestates;\
572        jn,A,B,Select,Start,L,,R|P;\
573        jp,B,Y,Select,Start,L,,R|P;\
574        I,\
575        Disk 1A,\
576        Disk 1B,\
577        Disk 2A,\
578        Disk 2B,\
579        Slot=DPAD|Save/Load=Start+DPAD,\
580        Active Slot 1,\
581        Active Slot 2,\
582        Active Slot 3,\
583        Active Slot 4,\
584        Save to state 1,\
585        Restore state 1,\
586        Save to state 2,\
587        Restore state 2,\
588        Save to state 3,\
589        Restore state 3,\
590        Save to state 4,\
591        Restore state 4;\
592        V,v123456";
593
594#[test]
595fn config_string_nes() {
596    let config = Config::from_str(CONFIG_STRING_NES);
597    assert!(config.is_ok(), "{:?}", config);
598}
599
600#[test]
601fn config_string_nes_menu() {
602    let config = Config::from_str(CONFIG_STRING_NES).unwrap();
603    config.as_core_settings(&StatusBitMap::new());
604}
605
606#[test]
607fn config_string_chess() {
608    // Taken from https://github.com/MiSTer-devel/Chess_MiSTer/blob/113b6f6/Chess.sv#L182
609    let config = Config::from_str(
610        [
611            "Chess;;",
612            "-;",
613            "O7,Opponent,AI,Human;",
614            "O46,AI Strength,1,2,3,4,5,6,7;",
615            "O23,AI Randomness,0,1,2,3;",
616            "O1,Player Color,White,Black;",
617            "O9,Boardview,White,Black;",
618            "OA,Overlay,Off,On;",
619            "-;",
620            "O8,Aspect Ratio,4:3,16:9;",
621            "-;",
622            "R0,Reset;",
623            "J1,Action,Cancel,SaveState,LoadState,Rewind;",
624            "jn,A,B;",
625            "jp,A,B;",
626            "V,v221106",
627        ]
628        .join("")
629        .as_str(),
630    );
631
632    assert!(config.is_ok(), "{:?}", config);
633    let config = config.unwrap();
634    assert!(config.settings.uart_mode.is_empty());
635
636    // From running the core on MiSTer:
637    //
638    // Status Bit Map:
639    //              Upper                          Lower
640    // 0         1         2         3          4         5         6
641    // 01234567890123456789012345678901 23456789012345678901234567890123
642    // 0123456789ABCDEFGHIJKLMNOPQRSTUV 0123456789ABCDEFGHIJKLMNOPQRSTUV
643    // XXXXXXXXXXX
644
645    let map = config.status_bit_map_mask();
646    let data = map.as_raw_slice();
647    let expected = [
648        0b00000111_11111111u16,
649        0b00000000_00000000,
650        0b00000000_00000000,
651        0b00000000_00000000,
652        0b00000000_00000000,
653        0b00000000_00000000,
654        0b00000000_00000000,
655        0b00000000_00000000,
656    ];
657    assert_eq!(
658        data, expected,
659        "actual: {:016b}{:016b}{:016b}{:016b}\nexpect: {:016b}{:016b}{:016b}{:016b}",
660        data[0], data[1], data[2], data[3], expected[0], expected[1], expected[2], expected[3]
661    );
662}
663
664#[test]
665fn config_string_ao486() {
666    // From https://github.com/MiSTer-devel/ao486_MiSTer/blob/09b29b2/ao486.sv#L199
667    let config = Config::from_str(
668        [
669            "AO486;UART115200:4000000(Turbo 115200),MIDI;",
670            "S0,IMGIMAVFD,Floppy A:;",
671            "S1,IMGIMAVFD,Floppy B:;",
672            "O12,Write Protect,None,A:,B:,A: & B:;",
673            "-;",
674            "S2,VHD,IDE 0-0;",
675            "S3,VHD,IDE 0-1;",
676            "-;",
677            "S4,VHDISOCUECHD,IDE 1-0;",
678            "S5,VHDISOCUECHD,IDE 1-1;",
679            "-;",
680            "oJM,CPU Preset,User Defined,~PC XT-7MHz,~PC AT-8MHz,~PC AT-10MHz,~PC AT-20MHz,~PS/2-20MHz,~386SX-25MHz,~386DX-33Mhz,~386DX-40Mhz,~486SX-33Mhz,~486DX-33Mhz,MAX (unstable);",
681            "-;",
682            "P1,Audio & Video;",
683            "P1-;",
684            "P1OMN,Aspect ratio,Original,Full Screen,[ARC1],[ARC2];",
685            "P1O4,VSync,60Hz,Variable;",
686            "P1O8,16/24bit mode,BGR,RGB;",
687            "P1O9,16bit format,1555,565;",
688            "P1OE,Low-Res,Native,4x;", "P1oDE,Scale,Normal,V-Integer,Narrower HV-Integer,Wider HV-Integer;", "P1-;", "P1O3,FM mode,OPL2,OPL3;", "P1OH,C/MS,Disable,Enable;", "P1OIJ,Speaker Volume,1,2,3,4;", "P1OKL,Audio Boost,No,2x,4x;", "P1oBC,Stereo Mix,none,25%,50%,100%;", "P1OP,MT32 Volume Ctl,MIDI,Line-In;", "P2,Hardware;", "P2o01,Boot 1st,Floppy/Hard Disk,Floppy,Hard Disk,CD-ROM;", "P2o23,Boot 2nd,NONE,Floppy,Hard Disk,CD-ROM;", "P2o45,Boot 3rd,NONE,Floppy,Hard Disk,CD-ROM;",
689            "P2-;",
690            "P2o6,IDE 1-0 CD Hot-Swap,Yes,No;",
691            "P2o7,IDE 1-1 CD Hot-Swap,No,Yes;",
692            "P2-;",
693            "P2OB,RAM Size,256MB,16MB;",
694            "P2-;",
695            "P2OA,USER I/O,MIDI,COM2;",
696            "P2-;",
697            "P2OCD,Joystick type,2 Buttons,4 Buttons,Gravis Pro,None;",
698            "P2oFG,Joystick Mode,2 Joysticks,2 Sticks,2 Wheels,4-axes Wheel;",
699            "P2oH,Joystick 1,Enabled,Disabled;",
700            "P2oI,Joystick 2,Enabled,Disabled;",
701            "h3P3,MT32-pi;",
702            "h3P3-;",
703            "h3P3OO,Use MT32-pi,Yes,No;",
704            "h3P3o9A,Show Info,No,Yes,LCD-On(non-FB),LCD-Auto(non-FB);",
705            "h3P3-;",
706            "h3P3-,Default Config:;",
707            "h3P3OQ,Synth,Munt,FluidSynth;",
708            "h3P3ORS,Munt ROM,MT-32 v1,MT-32 v2,CM-32L;",
709            "h3P3OTV,SoundFont,0,1,2,3,4,5,6,7;",
710            "h3P3-;",
711            "h3P3r8,Reset Hanging Notes;",
712            "-;",
713            "R0,Reset and apply HDD;",
714            "J,Button 1,Button 2,Button 3,Button 4,Start,Select,R1,L1,R2,L2;",
715            "jn,A,B,X,Y,Start,Select,R,L;",
716            "I,",
717            "MT32-pi: SoundFont #0,",
718            "MT32-pi: SoundFont #1,",
719            "MT32-pi: SoundFont #2,",
720            "MT32-pi: SoundFont #3,",
721            "MT32-pi: SoundFont #4,",
722            "MT32-pi: SoundFont #5,",
723            "MT32-pi: SoundFont #6,",
724            "MT32-pi: SoundFont #7,",
725            "MT32-pi: MT-32 v1,",
726            "MT32-pi: MT-32 v2,",
727            "MT32-pi: CM-32L,",
728            "MT32-pi: Unknown mode;",
729            "V,v123456"].join("").as_str()
730    );
731
732    assert!(config.is_ok(), "{:?}", config);
733}
734
735#[test]
736fn input_tester() {
737    let config = Config::from_str(
738        "InputTest;;-;\
739        O35,Scandoubler Fx,None,HQ2x,CRT 25%,CRT 50%,CRT 75%;\
740        OGJ,Analog Video H-Pos,0,-1,-2,-3,-4,-5,-6,-7,8,7,6,5,4,3,2,1;\
741        OKN,Analog Video V-Pos,0,-1,-2,-3,-4,-5,-6,-7,8,7,6,5,4,3,2,1;\
742        O89,Aspect ratio,Original,Full Screen,[ARC1],[ARC2];\
743        -;\
744        O6,Rotate video,Off,On;\
745        O7,Flip video,Off,On;\
746        -;\
747        RA,Open menu;\
748        -;\
749        F0,BIN,Load BIOS;\
750        F3,BIN,Load Sprite ROM;\
751        F4,YM,Load Music (YM5/6);\
752        -;\
753        R0,Reset;\
754        J,A,B,X,Y,L,R,Select,Start;\
755        V,v220825",
756    );
757
758    assert!(config.is_ok(), "{:?}", config);
759    let config = config.unwrap();
760    assert!(config.settings.uart_mode.is_empty());
761}
762
763#[test]
764fn config_string_gba() {
765    let config = Config::from_str(
766        "GBA;SS3E000000:80000;\
767        FS,GBA,Load,300C0000;\
768        -;\
769        C,Cheats;\
770        H1O[6],Cheats Enabled,Yes,No;\
771        -;\
772        D0R[12],Reload Backup RAM;\
773        D0R[13],Save Backup RAM;\
774        D0O[23],Autosave,Off,On;\
775        D0-;\
776        O[36],Savestates to SDCard,On,Off;\
777        O[43],Autoincrement Slot,Off,On;\
778        O[38:37],Savestate Slot,1,2,3,4;\
779        h4H3R[17],Save state (Alt-F1);\
780        h4H3R[18],Restore state (F1);\
781        -;\
782        P1,Video & Audio;\
783        P1-;\
784        P1O[33:32],Aspect ratio,Original,Full Screen,[ARC1],[ARC2];\
785        P1O[4:2],Scandoubler Fx,None,HQ2x,CRT 25%,CRT 50%,CRT 75%;\
786        P1O[35:34],Scale,Normal,V-Integer,Narrower HV-Integer,Wider HV-Integer;\
787        P1-;\
788        P1O[26:24],Modify Colors,Off,GBA 2.2,GBA 1.6,NDS 1.6,VBA 1.4,75%,50%,25%;\
789        P1-;\
790        P1O[39],Sync core to video,On,Off;\
791        P1O[10:9],Flickerblend,Off,Blend,30Hz;\
792        P1O[22:21],2XResolution,Off,Background,Sprites,Both;\
793        P1O[20],Spritelimit,Off,On;\
794        P1-;\
795        P1O[8:7],Stereo Mix,None,25%,50%,100%;\
796        P1O[19],Fast Forward Sound,On,Off;\
797        P2,Hardware;\
798        P2-;\
799        H6P2O[31:29],Solar Sensor,0%,15%,30%,42%,55%,70%,85%,100%;\
800        H2P2O[16],Turbo,Off,On;\
801        P2O[28],Homebrew BIOS(Reset!),Off,On;\
802        P3,Miscellaneous;\
803        P3-;\
804        P3O[15:14],Storage,Auto,SDRAM,DDR3;\
805        D5P3O[5],Pause when OSD is open,Off,On;\
806        P3O[27],Rewind Capture,Off,On;\
807        P3-;\
808        P3-,Only Romhacks or Crash!;\
809        P3O[40],GPIO HACK(RTC+Rumble),Off,On;\
810        P3O[42:41],Underclock CPU,0,1,2,3;\
811        - ;\
812        R0,Reset;\
813        J1,A,B,L,R,Select,Start,FastForward,Rewind,Savestates;\
814        jn,A,B,L,R,Select,Start,X,X;\
815        I,Load=DPAD Up|Save=Down|Slot=L+R,Active Slot 1,Active Slot 2,Active Slot 3,Active Slot 4,Save to state 1,Restore state 1,Save to state 2,Restore state 2,Save to state 3,Restore state 3,Save to state 4,Restore state 4,Rewinding...;\
816        V,v230803"
817    );
818    assert!(config.is_ok(), "{:?}", config);
819}