1use 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 pub save_support: bool,
81
82 pub index: u8,
84
85 pub extensions: Vec<FileExtension>,
88
89 pub label: Option<String>,
92
93 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#[derive(Debug, Clone)]
110pub enum ConfigMenu {
111 Empty(Option<String>),
113
114 Cheat(Option<String>),
116
117 DisableIf(u32, Box<ConfigMenu>),
119
120 DisableUnless(u32, Box<ConfigMenu>),
122
123 HideIf(u32, Box<ConfigMenu>),
125
126 HideUnless(u32, Box<ConfigMenu>),
128
129 Dip,
131
132 LoadFile(Box<LoadFileInfo>),
134
135 LoadFileAndRemember(Box<LoadFileInfo>),
138
139 MountSdCard {
141 slot: u8,
142 extensions: Vec<FileExtension>,
143 label: Option<String>,
144 },
145
146 Option {
155 bits: Range<u8>,
156 label: String,
157 choices: Vec<String>,
158 },
159
160 Trigger {
165 close_osd: bool,
166 index: u8,
167 label: String,
168 },
169
170 JoystickButtons {
175 keyboard: bool,
177
178 buttons: Vec<String>,
180 },
181
182 SnesButtonDefaultList {
183 buttons: Vec<String>,
184 },
185
186 SnesButtonDefaultPositionalList {
187 buttons: Vec<String>,
188 },
189
190 Page {
197 index: u8,
199
200 label: String,
202 },
203
204 PageItem(u8, Box<ConfigMenu>),
207
208 Info(Vec<String>),
211
212 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 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 pub name: String,
382
383 pub settings: settings::Settings,
385
386 pub menu: Vec<ConfigMenu>,
388}
389
390impl Config {
391 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 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 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#[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 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 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 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}