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