multiple state saving and loading working
This commit is contained in:
parent
5f54f10b3c
commit
435a1b41fc
|
@ -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
|
||||
|
|
28
src/main.rs
28
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)
|
||||
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:
|
||||
|
|
37
src/state.rs
37
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<PathBuf> {
|
||||
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<PathBuf> {
|
||||
pub fn find_next_filename(filepath: &PathBuf, new_ext: Option<&str>) -> Option<PathBuf> {
|
||||
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<PathBuf> {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn find_last_filename(filepath: &PathBuf) -> Option<PathBuf> {
|
||||
pub fn find_last_filename(filepath: &PathBuf, new_ext: Option<&str>) -> Option<PathBuf> {
|
||||
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::<Result<Vec<_>, 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::<Vec<PathBuf>>();
|
||||
save_states.sort();
|
||||
match save_states.len() {
|
||||
0 => None,
|
||||
_ => Some(save_states[save_states.len()-1].clone()),
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue