1use std::collections::HashMap;
7use std::fs;
8use std::path::PathBuf;
9use std::str::FromStr;
10
11use serde::Deserialize;
12
13use crate::cli::args::BuiltinPalette;
14use crate::cli::{CliArgs, OutputFormat, SavestateFormat, VideoExportMode, VideoFormat};
15
16pub const DEFAULT_VIDEO_FPS: &str = "1x";
18
19#[derive(Debug, Clone, Default, Deserialize)]
21#[serde(default)]
22pub struct ConfigFile {
23 #[serde(default)]
25 pub global: GlobalConfig,
26
27 #[serde(default)]
29 pub rom: RomConfig,
30
31 #[serde(default)]
33 pub savestate: SavestateConfig,
34
35 #[serde(default)]
37 pub memory: MemoryConfig,
38
39 #[serde(default)]
41 pub power: PowerConfig,
42
43 #[serde(default)]
45 pub palette: PaletteConfig,
46
47 #[serde(default)]
49 pub video: VideoConfig,
50
51 #[serde(default)]
53 pub execution: ExecutionConfig,
54
55 #[serde(default)]
57 pub output: OutputConfig,
58}
59
60#[derive(Debug, Clone, Default, Deserialize)]
61#[serde(default)]
62pub struct GlobalConfig {
63 pub quiet: Option<bool>,
64 pub verbose: Option<bool>,
65}
66
67#[derive(Debug, Clone, Default, Deserialize)]
68#[serde(default)]
69pub struct RomConfig {
70 pub path: Option<PathBuf>,
71 pub rom_info: Option<bool>,
72}
73
74#[derive(Debug, Clone, Default, Deserialize)]
75#[serde(default)]
76pub struct SavestateConfig {
77 pub load: Option<PathBuf>,
78 pub save: Option<PathBuf>,
79 pub state_stdin: Option<bool>,
80 pub state_stdout: Option<bool>,
81 pub save_on: Option<String>,
82 pub format: SavestateFormat,
83}
84
85#[derive(Debug, Clone, Default, Deserialize)]
86#[serde(default)]
87pub struct MemoryConfig {
88 pub read_cpu: Option<String>,
89 pub read_ppu: Option<String>,
90 pub dump_oam: Option<bool>,
91 pub dump_nametables: Option<bool>,
92 pub dump_palette: Option<bool>,
93 pub init_file: Option<PathBuf>,
94
95 #[serde(default)]
97 pub init_cpu: HashMap<String, Vec<u8>>,
98
99 #[serde(default)]
101 pub init_ppu: HashMap<String, Vec<u8>>,
102
103 #[serde(default)]
105 pub init_oam: HashMap<String, Vec<u8>>,
106}
107
108#[derive(Debug, Clone, Default, Deserialize)]
109#[serde(default)]
110pub struct PowerConfig {
111 pub no_power: Option<bool>,
112 pub reset: Option<bool>,
113}
114
115#[derive(Debug, Clone, Default, Deserialize)]
116#[serde(default)]
117pub struct PaletteConfig {
118 pub path: Option<PathBuf>,
119 pub builtin: Option<String>,
120}
121
122#[derive(Debug, Clone, Default, Deserialize)]
123#[serde(default)]
124pub struct VideoConfig {
125 pub screenshot: Option<PathBuf>,
126 pub screenshot_on: Option<String>,
127 pub video_path: Option<PathBuf>,
128 pub video_format: Option<String>,
129 pub video_fps: Option<String>,
131 pub video_mode: Option<String>,
133 pub video_scale: Option<String>,
134 pub renderer: Option<String>,
136}
137
138#[derive(Debug, Clone, Default, Deserialize)]
139#[serde(default)]
140pub struct ExecutionConfig {
141 pub cycles: Option<u128>,
142 pub frames: Option<u64>,
143 pub until_opcode: Option<String>,
144 pub until_mem: Option<Vec<String>>,
145 pub until_hlt: Option<bool>,
146 pub trace: Option<PathBuf>,
147 #[serde(default)]
148 pub breakpoints: Vec<String>,
149 #[serde(default)]
151 pub watch_mem: Vec<String>,
152 #[serde(default)]
154 pub stop_conditions: Vec<String>,
155}
156
157#[derive(Debug, Clone, Default, Deserialize)]
158#[serde(default)]
159pub struct OutputConfig {
160 pub path: Option<PathBuf>,
161 pub format: Option<String>,
162 pub json: Option<bool>,
164 pub toml: Option<bool>,
166 pub binary: Option<bool>,
168}
169
170impl ConfigFile {
171 pub fn load(path: &PathBuf) -> Result<Self, ConfigError> {
173 let content = fs::read_to_string(path).map_err(|e| ConfigError::IoError(e.to_string()))?;
174 toml::from_str(&content).map_err(|e| ConfigError::ParseError(e.to_string()))
175 }
176
177 pub fn merge_with_cli(&self, cli: &mut CliArgs) {
180 if !cli.quiet {
183 cli.quiet = self.global.quiet.unwrap_or(false);
184 }
185 if !cli.verbose {
186 cli.verbose = self.global.verbose.unwrap_or(false);
187 }
188
189 if cli.rom.rom.is_none() {
191 cli.rom.rom = self.rom.path.clone();
192 }
193 if !cli.rom.rom_info {
194 cli.rom.rom_info = self.rom.rom_info.unwrap_or(false);
195 }
196
197 if cli.savestate.load_state.is_none() {
199 cli.savestate.load_state = self.savestate.load.clone();
200 }
201 if cli.savestate.save_state.is_none() {
202 cli.savestate.save_state = self.savestate.save.clone();
203 }
204 if !cli.savestate.state_stdin {
205 cli.savestate.state_stdin = self.savestate.state_stdin.unwrap_or(false);
206 }
207 if !cli.savestate.state_stdout {
208 cli.savestate.state_stdout = self.savestate.state_stdout.unwrap_or(false);
209 }
210 if cli.savestate.save_state_on.is_none() {
211 cli.savestate.save_state_on = self.savestate.save_on.clone();
212 }
213 if cli.savestate.state_format == SavestateFormat::Binary {
214 cli.savestate.state_format = self.savestate.format;
215 }
216
217 if cli.memory.read_cpu.is_none() {
219 cli.memory.read_cpu = self.memory.read_cpu.clone();
220 }
221 if cli.memory.read_ppu.is_none() {
222 cli.memory.read_ppu = self.memory.read_ppu.clone();
223 }
224 if !cli.memory.dump_oam {
225 cli.memory.dump_oam = self.memory.dump_oam.unwrap_or(false);
226 }
227 if !cli.memory.dump_nametables {
228 cli.memory.dump_nametables = self.memory.dump_nametables.unwrap_or(false);
229 }
230 if !cli.memory.dump_palette {
231 cli.memory.dump_palette = self.memory.dump_palette.unwrap_or(false);
232 }
233 if cli.memory.init_file.is_none() {
234 cli.memory.init_file = self.memory.init_file.clone();
235 }
236 if cli.memory.init_cpu.is_empty() {
238 for (addr, values) in &self.memory.init_cpu {
239 let values_str = values
240 .iter()
241 .map(|v| format!("0x{:02X}", v))
242 .collect::<Vec<_>>()
243 .join(",");
244 cli.memory.init_cpu.push(format!("{}={}", addr, values_str));
245 }
246 }
247 if cli.memory.init_ppu.is_empty() {
249 for (addr, values) in &self.memory.init_ppu {
250 let values_str = values
251 .iter()
252 .map(|v| format!("0x{:02X}", v))
253 .collect::<Vec<_>>()
254 .join(",");
255 cli.memory.init_ppu.push(format!("{}={}", addr, values_str));
256 }
257 }
258 if cli.memory.init_oam.is_empty() {
260 for (addr, values) in &self.memory.init_oam {
261 let values_str = values
262 .iter()
263 .map(|v| format!("0x{:02X}", v))
264 .collect::<Vec<_>>()
265 .join(",");
266 cli.memory.init_oam.push(format!("{}={}", addr, values_str));
267 }
268 }
269
270 if !cli.power.no_power {
272 cli.power.no_power = self.power.no_power.unwrap_or(false);
273 }
274 if !cli.power.reset {
275 cli.power.reset = self.power.reset.unwrap_or(false);
276 }
277
278 if cli.palette.palette.is_none() {
280 cli.palette.palette = self.palette.path.clone();
281 }
282 if cli.palette.palette_builtin.is_none()
283 && let Some(ref builtin) = self.palette.builtin
284 {
285 cli.palette.palette_builtin = BuiltinPalette::from_str(builtin).ok();
286 }
287
288 if cli.video.screenshot.is_none() {
290 cli.video.screenshot = self.video.screenshot.clone();
291 }
292 if cli.video.screenshot_on.is_none() {
293 cli.video.screenshot_on = self.video.screenshot_on.clone();
294 }
295 if cli.video.video_path.is_none() {
296 cli.video.video_path = self.video.video_path.clone();
297 }
298 if let Some(ref fmt) = self.video.video_format {
299 if cli.video.video_format == VideoFormat::Raw {
301 cli.video.video_format = VideoFormat::from_str(fmt).unwrap_or(VideoFormat::Raw);
302 }
303 }
304 if cli.video.video_scale.is_none() {
305 if self.video.video_scale.is_none() {
306 cli.video.video_scale = Some("native".to_string());
307 } else {
308 cli.video.video_scale = self.video.video_scale.clone();
309 }
310 }
311 if cli.video.video_fps == DEFAULT_VIDEO_FPS
313 && let Some(ref fps) = self.video.video_fps
314 {
315 cli.video.video_fps = fps.clone();
316 }
317 if cli.video.video_mode == VideoExportMode::Accurate
319 && let Some(ref mode) = self.video.video_mode
320 {
321 cli.video.video_mode =
322 VideoExportMode::from_str(mode).unwrap_or(VideoExportMode::Accurate);
323 }
324 if cli.video.renderer.is_none() {
326 cli.video.renderer = self.video.renderer.clone();
327 }
328
329 if cli.execution.cycles.is_none() {
331 cli.execution.cycles = self.execution.cycles;
332 }
333 if cli.execution.frames.is_none() {
334 cli.execution.frames = self.execution.frames;
335 }
336 if cli.execution.until_opcode.is_none()
337 && let Some(ref op) = self.execution.until_opcode
338 {
339 cli.execution.until_opcode = parse_hex_u8_opt(op);
340 }
341 if cli.execution.until_mem.is_none() {
342 cli.execution.until_mem = self.execution.until_mem.clone();
343 }
344 if !cli.execution.until_hlt {
345 cli.execution.until_hlt = self.execution.until_hlt.unwrap_or(false);
346 }
347 if cli.execution.trace.is_none() {
348 cli.execution.trace = self.execution.trace.clone();
349 }
350 if cli.execution.breakpoint.is_empty() {
351 for bp in &self.execution.breakpoints {
352 if let Some(addr) = parse_hex_u16_opt(bp) {
353 cli.execution.breakpoint.push(addr);
354 }
355 }
356 }
357 if cli.execution.watch_mem.is_empty() {
358 cli.execution.watch_mem = self.execution.watch_mem.clone();
359 }
360
361 for cond in &self.execution.stop_conditions {
363 if let Some(rest) = cond.strip_prefix("pc:") {
364 if let Some(addr) = parse_hex_u16_opt(rest) {
366 cli.execution.breakpoint.push(addr);
367 }
368 } else if let Some(rest) = cond.strip_prefix("frames:") {
369 if cli.execution.frames.is_none() {
370 cli.execution.frames = rest.parse().ok();
371 }
372 } else if let Some(rest) = cond.strip_prefix("cycles:")
373 && cli.execution.cycles.is_none()
374 {
375 cli.execution.cycles = rest.parse().ok();
376 }
377 }
378
379 if cli.output.output.is_none() {
381 cli.output.output = self.output.path.clone();
382 }
383 if !cli.output.json && self.output.json.unwrap_or(false) {
386 cli.output.json = true;
387 }
388 if !cli.output.toml && self.output.toml.unwrap_or(false) {
389 cli.output.toml = true;
390 }
391 if !cli.output.binary && self.output.binary.unwrap_or(false) {
392 cli.output.binary = true;
393 }
394 if let Some(ref fmt) = self.output.format {
395 if cli.output.output_format == OutputFormat::Hex
397 && !cli.output.json
398 && !cli.output.toml
399 && !cli.output.binary
400 {
401 cli.output.output_format = OutputFormat::from_str(fmt).unwrap_or(OutputFormat::Hex);
402 }
403 }
404 }
405}
406
407fn parse_hex_u16_opt(s: &str) -> Option<u16> {
409 let s = s
410 .strip_prefix("0x")
411 .or_else(|| s.strip_prefix("0X"))
412 .unwrap_or(s);
413 u16::from_str_radix(s, 16).ok()
414}
415
416fn parse_hex_u8_opt(s: &str) -> Option<u8> {
418 let s = s
419 .strip_prefix("0x")
420 .or_else(|| s.strip_prefix("0X"))
421 .unwrap_or(s);
422 u8::from_str_radix(s, 16).ok()
423}
424
425#[derive(Debug, Clone)]
427pub enum ConfigError {
428 IoError(String),
429 ParseError(String),
430}
431
432impl std::fmt::Display for ConfigError {
433 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
434 match self {
435 ConfigError::IoError(e) => write!(f, "Failed to read config file: {}", e),
436 ConfigError::ParseError(e) => write!(f, "Failed to parse config file: {}", e),
437 }
438 }
439}
440
441impl std::error::Error for ConfigError {}
442
443#[cfg(test)]
444mod tests {
445 use super::*;
446
447 #[test]
448 fn test_parse_empty_config() {
449 let config: ConfigFile = toml::from_str("").unwrap();
450 assert!(config.rom.path.is_none());
451 }
452
453 #[test]
454 fn test_parse_basic_config() {
455 let toml_str = r#"
456 [rom]
457 path = "game.nes"
458
459 [execution]
460 frames = 100
461 "#;
462 let config: ConfigFile = toml::from_str(toml_str).unwrap();
463 assert_eq!(config.rom.path, Some(PathBuf::from("game.nes")));
464 assert_eq!(config.execution.frames, Some(100));
465 }
466
467 #[test]
468 fn test_parse_memory_init() {
469 let toml_str = r#"
470 [memory.init_cpu]
471 "0x0050" = [0xFF, 0x00, 0x10]
472 "0x0060" = [0x01]
473 "#;
474 let config: ConfigFile = toml::from_str(toml_str).unwrap();
475 assert_eq!(
476 config.memory.init_cpu.get("0x0050"),
477 Some(&vec![0xFF, 0x00, 0x10])
478 );
479 assert_eq!(config.memory.init_cpu.get("0x0060"), Some(&vec![0x01]));
480 }
481
482 #[test]
483 fn test_parse_stop_conditions() {
484 let toml_str = r#"
485 [execution]
486 stop_conditions = ["pc:0x8500", "frames:3600"]
487 "#;
488 let config: ConfigFile = toml::from_str(toml_str).unwrap();
489 assert_eq!(config.execution.stop_conditions.len(), 2);
490 }
491}