diff --git a/README.md b/README.md index d9c2a58..db09b3d 100644 --- a/README.md +++ b/README.md @@ -20,10 +20,11 @@ ___________________ | Left | Left | | Right | Right | ------------------- -Save state: F5 -Load state: F9 -(Saving state a second time overwrites the first unless you copy/rename the `.dat` file. You can drag and drop any `.dat` file onto the window to load it.) ``` + +## Save states +To save the state of the game at any time, press F5. To load the most recent save state, press F9. If the game is called `mygame.nes`, the save state files will be called `mygame-X.dat` where `X` is a number. To load any previous save state, drag and drop a `.dat` file onto the window. + The code aims to follow the explanations from the [NES dev wiki](https://wiki.nesdev.com/w/index.php/NES_reference_guide) where possible, especially in the PPU, and the comments quote from it often. Thanks to everyone who contributes to that wiki/forum, and to Michael Fogleman's [NES](https://github.com/fogleman/nes) and Scott Ferguson's [Fergulator](https://github.com/scottferg/Fergulator) for getting me unstuck at several points. ## Compilation and use @@ -48,8 +49,6 @@ The code aims to follow the explanations from the [NES dev wiki](https://wiki.ne - Better GUI -- Better save state handling - - Player 2 controller? ## Known problem games diff --git a/src/main.rs b/src/main.rs index 4f4a844..179b471 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,15 +13,15 @@ use apu::Apu; use cartridge::get_mapper; use input::poll_buttons; use screen::{init_window, draw_pixel, draw_to_window}; -use state::{save_state, load_state, change_file_extension, find_next_filename}; +use state::{save_state, load_state, find_next_filename, find_last_filename}; +use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex}; use std::time::{Instant, Duration}; use sdl2::keyboard::Keycode; use sdl2::EventPump; use sdl2::event::Event; use sdl2::pixels::PixelFormatEnum; -use std::path::Path; // use cpuprofiler::PROFILER; @@ -47,6 +47,7 @@ fn main() -> Result<(), String> { // Initialize hardware components let filename = get_filename(); + let filepath = Path::new(&filename).to_path_buf(); let mapper = get_mapper(filename.clone()); let ppu = Ppu::new(mapper.clone()); let apu = Apu::new(); @@ -92,7 +93,7 @@ fn main() -> Result<(), String> { std::thread::sleep(timer + Duration::from_millis(1000/60) - now); } timer = Instant::now(); - if !process_events(&mut event_pump, &filename, &mut cpu) { + if !process_events(&mut event_pump, &filepath, &mut cpu) { break 'running; } } @@ -121,27 +122,31 @@ fn get_filename() -> String { argv[1].clone() } -fn process_events(event_pump: &mut EventPump, filename: &str, cpu: &mut Cpu) -> bool { +fn process_events(event_pump: &mut EventPump, filepath: &PathBuf, cpu: &mut Cpu) -> bool { for event in event_pump.poll_iter() { match event { Event::Quit {..} | Event::KeyDown { keycode: Some(Keycode::Escape), .. } => return false, Event::KeyDown{ keycode: Some(Keycode::F5), .. } => { - let dat_filename = change_file_extension(&filename, "dat").unwrap(); - println!("{:?}", find_next_filename(&dat_filename)); - let res: Result<(), String> = save_state(cpu, dat_filename) + let save_file = find_next_filename(filepath, Some("dat")) + .expect("could not generate save state filename"); + let res: Result<(), String> = save_state(cpu, &save_file) .or_else(|e| {println!("{}", e); Ok(())}); res.unwrap(); }, Event::KeyDown{ keycode: Some(Keycode::F9), .. } => { - let dat_filename = change_file_extension(&filename, "dat").unwrap(); - let res: Result<(), String> = load_state(cpu, dat_filename) - .or_else(|e| {println!("{}", e); Ok(())}); - res.unwrap(); + match find_last_filename(filepath, Some("dat")) { + Some(p) => { + let res: Result<(), String> = load_state(cpu, &p) + .or_else(|e| { println!("{}", e); Ok(()) } ); + res.unwrap(); + }, + None => println!("no save state found for {:?}", filepath) + } }, Event::DropFile{ timestamp: _t, window_id: _w, filename: f } => { let p = Path::new(&f).to_path_buf(); - let res: Result<(), String> = load_state(cpu, p) + let res: Result<(), String> = load_state(cpu, &p) .or_else(|e| {println!("{}", e); Ok(())}); res.unwrap(); }, @@ -158,8 +163,7 @@ TODO: - DMC audio channel - untangle CPU and APU/PPU? - GUI? drag and drop ROMs? -- reset function -- save states: multiple, search/select, and generalized "find file by different extension" functionality +- reset function/button Timing notes: diff --git a/src/state.rs b/src/state.rs index 4eb3eb3..be794ce 100644 --- a/src/state.rs +++ b/src/state.rs @@ -14,7 +14,7 @@ struct SaveState { apu: apu::serialize::ApuData, } -pub fn save_state(cpu: &cpu::Cpu, save_file: PathBuf) -> Result<(), String> { +pub fn save_state(cpu: &cpu::Cpu, save_file: &PathBuf) -> Result<(), String> { let data = SaveState{ cpu: cpu.save_state(), ppu: cpu.ppu.save_state(), @@ -30,7 +30,7 @@ pub fn save_state(cpu: &cpu::Cpu, save_file: PathBuf) -> Result<(), String> { Ok(()) } -pub fn load_state(cpu: &mut cpu::Cpu, save_file: PathBuf) -> Result<(), String> { +pub fn load_state(cpu: &mut cpu::Cpu, save_file: &PathBuf) -> Result<(), String> { if Path::new(&save_file).exists() { let mut f = File::open(save_file.clone()) .map_err(|e| e.to_string())?; @@ -51,18 +51,10 @@ pub fn load_state(cpu: &mut cpu::Cpu, save_file: PathBuf) -> Result<(), String> } } -pub fn change_file_extension(filename: &str, extension: &str) -> Option { - let path = Path::new(filename).parent()?; - let stem = Path::new(&filename).file_stem()?; - let mut save_file = path.join(stem); - save_file.set_extension(extension); - Some(save_file) -} - -pub fn find_next_filename(filepath: &PathBuf) -> Option { +pub fn find_next_filename(filepath: &PathBuf, new_ext: Option<&str>) -> Option { let path = filepath.parent()?.to_str()?; let stem = filepath.file_stem()?.to_str()?; - let ext = filepath.extension()?.to_str()?; + let ext = new_ext.or(Some(filepath.extension()?.to_str()?)).unwrap(); let sep = std::path::MAIN_SEPARATOR.to_string(); let mut i = 0; loop { @@ -75,13 +67,24 @@ pub fn find_next_filename(filepath: &PathBuf) -> Option { } } -pub fn find_last_filename(filepath: &PathBuf) -> Option { +pub fn find_last_filename(filepath: &PathBuf, new_ext: Option<&str>) -> Option { let path = filepath.parent()?; let stem = filepath.file_stem()?.to_str()?; + let ext = new_ext.or(Some(filepath.extension()?.to_str()?)).unwrap(); let files = std::fs::read_dir(path).expect("couldn't read directory"); - let save_states = files.filter(|f| { - let n = f.unwrap().file_name().to_str().unwrap(); - &n[..stem.len()] == stem && &n[n.len()-4..] == ".dat" - }).collect::, std::io::Error>>().unwrap(); + let mut save_states = files + .map(|f| f.unwrap().path() ) + .filter(|p| { + let pfs = p.file_name().unwrap().to_str().unwrap(); + pfs.len() >= stem.len() + && pfs.len() >= ext.len() + && &pfs[..stem.len()] == stem + && &pfs[pfs.len()-ext.len()..] == ext + }) + .collect::>(); save_states.sort(); + match save_states.len() { + 0 => None, + _ => Some(save_states[save_states.len()-1].clone()), + } }