//! System save state (BOS and [BESS](https://github.com/LIJI32/SameBoy/blob/master/BESS.md) formats) functions and structures. //! //! The BOS (Boytacean Save) format is a custom save state format that contains the emulator state and the frame buffer. //! Its serialization includes header, info, image buffer and then a BESS (Best Effort Save State) footer with the state itself. //! //! The [BESS](https://github.com/LIJI32/SameBoy/blob/master/BESS.md) format is a format developed by the [SameBoy](https://sameboy.github.io/) emulator and is used to store the emulator state //! in agnostic and compatible way. use boytacean_common::{ data::{ read_bytes, read_into, read_u16, read_u32, read_u64, read_u8, write_bytes, write_u16, write_u32, write_u64, write_u8, }, error::Error, util::{save_bmp, timestamp}, }; use boytacean_encoding::zippy::{decode_zippy, encode_zippy}; use std::{ convert::TryInto, fmt::{self, Display, Formatter}, fs::File, io::{Cursor, Read, Seek, SeekFrom, Write}, mem::size_of, vec, }; use crate::{ disable_pedantic, enable_pedantic, gb::{GameBoy, GameBoyDevice, GameBoyMode, GameBoySpeed}, info::Info, ppu::{DISPLAY_HEIGHT, DISPLAY_WIDTH, FRAME_BUFFER_SIZE}, rom::{CgbMode, MbcType}, }; #[cfg(feature = "wasm")] use wasm_bindgen::prelude::*; /// Magic string for the BOSC (Boytacean Save Compressed) format. pub const BOSC_MAGIC: &str = "BOSC"; /// Magic string ("BOSC") in little endian unsigned 32 bit format. pub const BOSC_MAGIC_UINT: u32 = 0x43534f42; /// Magic string for the BOS (Boytacean Save) format. pub const BOS_MAGIC: &str = "BOS\0"; /// Magic string ("BOS\0") in little endian unsigned 32 bit format. pub const BOS_MAGIC_UINT: u32 = 0x00534f42; /// Current version of the BOS (Boytacean Save Compressed) format. pub const BOSC_VERSION: u8 = 1; /// Current version of the BOS (Boytacean Save) format. pub const BOS_VERSION: u8 = 1; /// Magic number for the BESS file format. pub const BESS_MAGIC: u32 = 0x53534542; /// Represents the different formats for the state storage /// and retrieval. /// /// Different formats will have different levels of detail /// and will require different amounts of data to be /// stored and retrieved. #[derive(Clone, Copy, PartialEq, Eq, Debug)] #[cfg_attr(feature = "wasm", wasm_bindgen)] pub enum StateFormat { /// Minimal state format, meaning that only the most basic /// elements of the component will be stored and retrieved. Minimal = 1, /// Partial state format, meaning that only the essential /// elements of the component will be stored and retrieved. /// All the remaining data, should inferred or computed. Partial = 2, /// Full state format, meaning that every single element /// of the component will be stored and retrieved. This /// should included redundant and calculated data. Full = 3, } impl From<u8> for StateFormat { fn from(value: u8) -> Self { match value { 1 => Self::Minimal, 2 => Self::Partial, 3 => Self::Full, _ => Self::Partial, } } } /// Represents a component that is able to store and retrieve /// the state of its internal structure. /// /// This trait is used to define the behavior of the state /// components that are used to store the emulator state. /// /// Ideally each of Game Boy's components should implement /// this trait to allow the state to be saved and restored /// in a consistent way. pub trait StateComponent { fn state(&self, format: Option<StateFormat>) -> Result<Vec<u8>, Error>; fn set_state(&mut self, data: &[u8], format: Option<StateFormat>) -> Result<(), Error>; } #[derive(Clone, Copy, PartialEq, Eq, Debug)] #[cfg_attr(feature = "wasm", wasm_bindgen)] pub enum SaveStateFormat { /// Boytacean Save Compressed format (BOSC). /// This format uses the Zippy compression algorithm /// to compress the underlying BOS contents. Bosc = 1, /// Boytacean Save format (uncompressed) (BOS). Bos = 2, /// Best Effort Save State format (BESS). Bess = 3, } impl SaveStateFormat { pub fn description(&self) -> String { match self { Self::Bosc => String::from("BOSC"), Self::Bos => String::from("BOS"), Self::Bess => String::from("BESS"), } } pub fn from_string(value: &str) -> Self { match value { "BOSC" => Self::Bosc, "BOS" => Self::Bos, "BESS" => Self::Bess, _ => Self::Bos, } } } impl From<&str> for SaveStateFormat { fn from(value: &str) -> Self { Self::from_string(value) } } impl Display for SaveStateFormat { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { write!(f, "{}", self.description()) } } #[derive(Clone, Copy)] pub enum BosBlockKind { Info = 0x01, ImageBuffer = 0x02, DeviceState = 0x03, Unknown = 0xff, } impl BosBlockKind { fn from_u8(value: u8) -> Self { match value { 0x01 => Self::Info, 0x02 => Self::ImageBuffer, 0x03 => Self::DeviceState, _ => Self::Unknown, } } pub fn description(&self) -> String { match self { Self::Info => String::from("Info"), Self::ImageBuffer => String::from("ImageBuffer"), Self::DeviceState => String::from("DeviceState"), Self::Unknown => String::from("Unknown"), } } } impl Display for BosBlockKind { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { write!(f, "{}", self.description()) } } impl From<u8> for BosBlockKind { fn from(value: u8) -> Self { Self::from_u8(value) } } #[cfg_attr(feature = "wasm", wasm_bindgen)] pub struct FromGbOptions { thumbnail: bool, state_format: Option<StateFormat>, agent: Option<String>, agent_version: Option<String>, } #[cfg_attr(feature = "wasm", wasm_bindgen)] impl FromGbOptions { pub fn new( thumbnail: bool, state_format: Option<StateFormat>, agent: Option<String>, agent_version: Option<String>, ) -> Self { Self { thumbnail, state_format, agent, agent_version, } } } impl Default for FromGbOptions { fn default() -> Self { Self { thumbnail: true, state_format: None, agent: None, agent_version: None, } } } #[cfg_attr(feature = "wasm", wasm_bindgen)] pub struct ToGbOptions { reload: bool, } impl ToGbOptions { pub fn new(reload: bool) -> Self { Self { reload } } } impl Default for ToGbOptions { fn default() -> Self { Self { reload: true } } } pub trait Serialize { /// Writes the data from the internal structure into the /// provided buffer. fn write<W: Write + Seek>(&mut self, writer: &mut W) -> Result<(), Error>; /// Reads the data from the provided buffer and populates /// the internal structure with it. fn read<R: Read + Seek>(&mut self, reader: &mut R) -> Result<(), Error>; } pub trait State { /// Obtains a new instance of the state from the provided /// `GameBoy` instance and returns it. fn from_gb(gb: &mut GameBoy) -> Result<Self, Error> where Self: Sized; /// Applies the state to the provided `GameBoy` instance. fn to_gb(&self, gb: &mut GameBoy) -> Result<(), Error>; } pub trait StateBox { /// Obtains a new instance of the state from the provided /// `GameBoy` instance and returns it as a boxed value. fn from_gb(gb: &mut GameBoy, options: &FromGbOptions) -> Result<Box<Self>, Error> where Self: Sized; /// Applies the state to the provided `GameBoy` instance. fn to_gb(&self, gb: &mut GameBoy, options: &ToGbOptions) -> Result<(), Error>; } pub trait StateConfig { /// Obtains the Game Boy execution mode expected by the /// state instance. fn mode(&self) -> Result<GameBoyMode, Error>; } pub trait StateInfo { fn timestamp(&self) -> Result<u64, Error>; fn agent(&self) -> Result<String, Error>; fn model(&self) -> Result<String, Error>; fn title(&self) -> Result<String, Error>; fn image_eager(&self) -> Result<Vec<u8>, Error>; fn has_image(&self) -> bool; } #[cfg_attr(feature = "wasm", wasm_bindgen)] #[derive(Default)] pub struct BoscState { magic: u32, version: u8, bos: BosState, } impl BoscState { /// Checks if the data contained in the provided /// buffer represents a valid BOSC (Boytacean Save /// Compressed) file structure, thought magic /// string validation. pub fn is_bosc<R: Read + Seek>(reader: &mut R) -> Result<bool, Error> { let magic = read_u32(reader)?; reader.rewind()?; Ok(magic == BOSC_MAGIC_UINT) } pub fn verify(&self) -> Result<(), Error> { if self.magic != BOSC_MAGIC_UINT { return Err(Error::DataError(String::from("Invalid magic"))); } if self.version != BOSC_VERSION { return Err(Error::DataError(format!( "Invalid version, expected {BOS_VERSION}, got {}", self.version ))); } self.bos.verify()?; Ok(()) } } impl Serialize for BoscState { fn write<W: Write + Seek>(&mut self, writer: &mut W) -> Result<(), Error> { write_u32(writer, self.magic)?; write_u8(writer, self.version)?; let mut cursor = Cursor::new(vec![]); self.bos.write(&mut cursor)?; let bos_compressed = encode_zippy(&cursor.into_inner(), None, None)?; write_bytes(writer, &bos_compressed)?; Ok(()) } fn read<R: Read + Seek>(&mut self, reader: &mut R) -> Result<(), Error> { self.magic = read_u32(reader)?; self.version = read_u8(reader)?; let mut bos_compressed = vec![]; reader.read_to_end(&mut bos_compressed)?; let bos_buffer = decode_zippy(&bos_compressed, None)?; let mut bos_cursor = Cursor::new(bos_buffer); self.bos.read(&mut bos_cursor)?; Ok(()) } } impl StateBox for BoscState { fn from_gb(gb: &mut GameBoy, options: &FromGbOptions) -> Result<Box<Self>, Error> { Ok(Box::new(Self { magic: BOSC_MAGIC_UINT, version: BOSC_VERSION, bos: *BosState::from_gb(gb, options)?, })) } fn to_gb(&self, gb: &mut GameBoy, options: &ToGbOptions) -> Result<(), Error> { self.verify()?; self.bos.to_gb(gb, options)?; Ok(()) } } impl StateConfig for BoscState { fn mode(&self) -> Result<GameBoyMode, Error> { self.bos.mode() } } #[cfg_attr(feature = "wasm", wasm_bindgen)] #[derive(Default)] pub struct BosState { magic: u32, version: u8, block_count: u8, info: Option<BosInfo>, image_buffer: Option<BosImageBuffer>, device_states: Vec<BosDeviceState>, bess: BessState, } impl BosState { /// Checks if the data contained in the provided /// buffer represents a valid BOS (Boytacean Save) /// file structure, thought magic string validation. pub fn is_bos<R: Read + Seek>(reader: &mut R) -> Result<bool, Error> { let magic = read_u32(reader)?; reader.rewind()?; Ok(magic == BOS_MAGIC_UINT) } pub fn verify(&self) -> Result<(), Error> { if self.magic != BOS_MAGIC_UINT { return Err(Error::CustomError(String::from("Invalid magic"))); } if self.version != BOS_VERSION { return Err(Error::CustomError(format!( "Invalid version, expected {BOS_VERSION}, got {}", self.version ))); } self.bess.verify()?; Ok(()) } pub fn save_image_bmp(&self, file_path: &str) -> Result<(), Error> { if let Some(image_buffer) = &self.image_buffer { image_buffer.save_bmp(file_path)?; Ok(()) } else { Err(Error::CustomError(String::from("No image buffer found"))) } } fn build_block_count(&self) -> u8 { let mut count = 0_u8; if self.info.is_some() { count += 1; } if self.image_buffer.is_some() { count += 1; } count += self.device_states.len() as u8; count } } impl StateInfo for BosState { fn timestamp(&self) -> Result<u64, Error> { if let Some(info) = &self.info { Ok(info.timestamp) } else { Err(Error::CustomError(String::from("No timestamp available"))) } } fn agent(&self) -> Result<String, Error> { if let Some(info) = &self.info { Ok(format!("{}/{}", info.agent, info.agent_version)) } else { Err(Error::CustomError(String::from("No agent available"))) } } fn model(&self) -> Result<String, Error> { if let Some(info) = &self.info { Ok(info.model.clone()) } else { Err(Error::CustomError(String::from("No model available"))) } } fn title(&self) -> Result<String, Error> { self.bess.title() } fn image_eager(&self) -> Result<Vec<u8>, Error> { if let Some(image_buffer) = &self.image_buffer { Ok(image_buffer.image.to_vec()) } else { Err(Error::CustomError(String::from("No image available"))) } } fn has_image(&self) -> bool { self.image_buffer.is_some() } } #[cfg(feature = "wasm")] #[cfg_attr(feature = "wasm", wasm_bindgen)] impl BosState { pub fn timestamp_wa(&self) -> Result<u64, String> { Ok(Self::timestamp(self)?) } pub fn agent_wa(&self) -> Result<String, String> { Ok(Self::agent(self)?) } pub fn model_wa(&self) -> Result<String, String> { Ok(Self::model(self)?) } pub fn title_wa(&self) -> Result<String, String> { Ok(Self::title(self)?) } pub fn image_eager_wa(&self) -> Result<Vec<u8>, String> { Ok(Self::image_eager(self)?) } pub fn has_image_wa(&self) -> bool { self.has_image() } } impl Serialize for BosState { fn write<W: Write + Seek>(&mut self, writer: &mut W) -> Result<(), Error> { self.block_count = self.build_block_count(); write_u32(writer, self.magic)?; write_u8(writer, self.version)?; write_u8(writer, self.block_count)?; if let Some(info) = &mut self.info { info.write(writer)?; } if let Some(image_buffer) = &mut self.image_buffer { image_buffer.write(writer)?; } for device_state in &mut self.device_states { device_state.write(writer)?; } self.bess.write(writer)?; Ok(()) } fn read<R: Read + Seek>(&mut self, reader: &mut R) -> Result<(), Error> { self.magic = read_u32(reader)?; self.version = read_u8(reader)?; self.block_count = read_u8(reader)?; for _ in 0..self.block_count { let block = BosBlock::from_data(reader)?; let offset = -((size_of::<u8>() + size_of::<u16>() + size_of::<u32>()) as i64); reader.seek(SeekFrom::Current(offset))?; match block.kind { BosBlockKind::Info => { self.info = Some(BosInfo::from_data(reader)?); } BosBlockKind::ImageBuffer => { self.image_buffer = Some(BosImageBuffer::from_data(reader)?); } BosBlockKind::DeviceState => { self.device_states.push(BosDeviceState::from_data(reader)?); } _ => { reader.seek(SeekFrom::Current(-offset))?; reader.seek(SeekFrom::Current(block.size as i64))?; } } } self.block_count = self.build_block_count(); self.bess.read(reader)?; Ok(()) } } impl StateBox for BosState { fn from_gb(gb: &mut GameBoy, options: &FromGbOptions) -> Result<Box<Self>, Error> { Ok(Box::new(Self { magic: BOS_MAGIC_UINT, version: BOS_VERSION, block_count: 2, info: Some(*<BosInfo as StateBox>::from_gb(gb, options)?), image_buffer: if options.thumbnail { Some(BosImageBuffer::from_gb(gb)?) } else { None }, device_states: vec![ BosDeviceState::from_gb(gb, GameBoyDevice::Cpu, options)?, BosDeviceState::from_gb(gb, GameBoyDevice::Ppu, options)?, BosDeviceState::from_gb(gb, GameBoyDevice::Apu, options)?, BosDeviceState::from_gb(gb, GameBoyDevice::Dma, options)?, BosDeviceState::from_gb(gb, GameBoyDevice::Pad, options)?, BosDeviceState::from_gb(gb, GameBoyDevice::Timer, options)?, ], bess: *BessState::from_gb(gb, options)?, })) } fn to_gb(&self, gb: &mut GameBoy, options: &ToGbOptions) -> Result<(), Error> { self.verify()?; self.bess.to_gb(gb, options)?; for device_state in &self.device_states { device_state.to_gb(gb, options)?; } Ok(()) } } impl StateConfig for BosState { fn mode(&self) -> Result<GameBoyMode, Error> { self.bess.mode() } } pub struct BosBlock { kind: BosBlockKind, version: u16, size: u32, } impl BosBlock { pub fn new(kind: BosBlockKind, version: u16, size: u32) -> Self { Self { kind, version, size, } } pub fn from_data<R: Read + Seek>(reader: &mut R) -> Result<Self, Error> { let mut instance = Self::default(); instance.read(reader)?; Ok(instance) } pub fn description(&self) -> String { format!("{} version={} size={}", self.kind, self.version, self.size) } } impl Serialize for BosBlock { fn write<W: Write + Seek>(&mut self, writer: &mut W) -> Result<(), Error> { write_u8(writer, self.kind as u8)?; write_u16(writer, self.version)?; write_u32(writer, self.size)?; Ok(()) } fn read<R: Read + Seek>(&mut self, reader: &mut R) -> Result<(), Error> { let check = self.version != 0; let expected_version = self.version; self.kind = read_u8(reader)?.into(); self.version = read_u16(reader)?; self.size = read_u32(reader)?; if check && self.version != expected_version { return Err(Error::DataError(format!( "Invalid version, expected {expected_version}, got {} for block ({})", self.version, self ))); } Ok(()) } } impl Default for BosBlock { fn default() -> Self { Self::new(BosBlockKind::Info, 0, 0) } } impl Display for BosBlock { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { write!(f, "{}", self.description()) } } pub struct BosInfo { header: BosBlock, timestamp: u64, agent: String, agent_version: String, model: String, } impl BosInfo { pub fn new(model: String, timestamp: u64, agent: String, agent_version: String) -> Self { Self { header: BosBlock::new( BosBlockKind::Info, 1, (size_of::<u64>() + size_of::<u8>() * agent.len() + size_of::<u8>() * agent_version.len() + size_of::<u8>() * model.len() + size_of::<u32>() * 4) as u32, ), model, timestamp, agent, agent_version, } } pub fn from_data<R: Read + Seek>(reader: &mut R) -> Result<Self, Error> { let mut instance = Self::default(); instance.read(reader)?; Ok(instance) } } impl Serialize for BosInfo { fn write<W: Write + Seek>(&mut self, writer: &mut W) -> Result<(), Error> { self.header.write(writer)?; write_u32(writer, size_of::<u64>() as u32)?; write_u64(writer, self.timestamp)?; write_u32(writer, self.agent.as_bytes().len() as u32)?; write_bytes(writer, self.agent.as_bytes())?; write_u32(writer, self.agent_version.as_bytes().len() as u32)?; write_bytes(writer, self.agent_version.as_bytes())?; write_u32(writer, self.model.as_bytes().len() as u32)?; write_bytes(writer, self.model.as_bytes())?; Ok(()) } fn read<R: Read + Seek>(&mut self, reader: &mut R) -> Result<(), Error> { self.header.read(reader)?; read_u32(reader)?; self.timestamp = read_u64(reader)?; let buffer_len = read_u32(reader)? as usize; self.agent = String::from_utf8(read_bytes(reader, buffer_len)?)?; let buffer_len = read_u32(reader)? as usize; self.agent_version = String::from_utf8(read_bytes(reader, buffer_len)?)?; let buffer_len = read_u32(reader)? as usize; self.model = String::from_utf8(read_bytes(reader, buffer_len)?)?; Ok(()) } } impl State for BosInfo { fn from_gb(gb: &mut GameBoy) -> Result<Self, Error> { let timestamp = timestamp(); Ok(Self::new( gb.mode().to_string(Some(true)), timestamp, Info::name_lower(), Info::version(), )) } fn to_gb(&self, _gb: &mut GameBoy) -> Result<(), Error> { Ok(()) } } impl StateBox for BosInfo { fn from_gb(gb: &mut GameBoy, options: &FromGbOptions) -> Result<Box<Self>, Error> where Self: Sized, { let timestamp = timestamp(); Ok(Box::new(Self::new( gb.mode().to_string(Some(true)), timestamp, options.agent.clone().unwrap_or(Info::name_lower()), options.agent_version.clone().unwrap_or(Info::version()), ))) } fn to_gb(&self, _gb: &mut GameBoy, _options: &ToGbOptions) -> Result<(), Error> { Ok(()) } } impl Default for BosInfo { fn default() -> Self { Self::new(String::from(""), 0, String::from(""), String::from("")) } } pub struct BosImageBuffer { header: BosBlock, image: [u8; FRAME_BUFFER_SIZE], } impl BosImageBuffer { pub fn new(image: [u8; FRAME_BUFFER_SIZE]) -> Self { Self { header: BosBlock::new( BosBlockKind::ImageBuffer, 1, (size_of::<u8>() * FRAME_BUFFER_SIZE) as u32, ), image, } } pub fn from_data<R: Read + Seek>(reader: &mut R) -> Result<Self, Error> { let mut instance = Self::default(); instance.read(reader)?; Ok(instance) } pub fn save_bmp(&self, file_path: &str) -> Result<(), Error> { save_bmp( file_path, &self.image, DISPLAY_WIDTH as u32, DISPLAY_HEIGHT as u32, )?; Ok(()) } } impl Serialize for BosImageBuffer { fn write<W: Write + Seek>(&mut self, writer: &mut W) -> Result<(), Error> { self.header.write(writer)?; write_bytes(writer, &self.image)?; Ok(()) } fn read<R: Read + Seek>(&mut self, reader: &mut R) -> Result<(), Error> { self.header.read(reader)?; read_into(reader, &mut self.image)?; Ok(()) } } impl State for BosImageBuffer { fn from_gb(gb: &mut GameBoy) -> Result<Self, Error> { Ok(Self::new(gb.ppu_i().frame_buffer_raw())) } fn to_gb(&self, _gb: &mut GameBoy) -> Result<(), Error> { Ok(()) } } impl Default for BosImageBuffer { fn default() -> Self { Self::new([0x00; FRAME_BUFFER_SIZE]) } } pub struct BosDeviceState { header: BosBlock, device: GameBoyDevice, format: StateFormat, state: Vec<u8>, } impl BosDeviceState { pub fn new(device: GameBoyDevice, format: StateFormat, state: Vec<u8>) -> Self { Self { header: BosBlock::new( BosBlockKind::DeviceState, 1, (size_of::<u8>() + size_of::<u8>() + state.len()) as u32, ), device, format, state, } } pub fn from_data<R: Read + Seek>(reader: &mut R) -> Result<Self, Error> { let mut instance = Self::default(); instance.read(reader)?; Ok(instance) } fn from_gb( gb: &mut GameBoy, device: GameBoyDevice, options: &FromGbOptions, ) -> Result<Self, Error> { let format: StateFormat = options.state_format.unwrap_or(StateFormat::Partial); match device { GameBoyDevice::Cpu => Ok(Self::new(device, format, gb.cpu_i().state(Some(format))?)), GameBoyDevice::Ppu => Ok(Self::new(device, format, gb.ppu_i().state(Some(format))?)), GameBoyDevice::Apu => Ok(Self::new(device, format, gb.apu_i().state(Some(format))?)), GameBoyDevice::Dma => Ok(Self::new(device, format, gb.dma_i().state(Some(format))?)), GameBoyDevice::Pad => Ok(Self::new(device, format, gb.pad_i().state(Some(format))?)), GameBoyDevice::Timer => { Ok(Self::new(device, format, gb.timer_i().state(Some(format))?)) } _ => Err(Error::NotImplemented), } } fn to_gb(&self, gb: &mut GameBoy, _options: &ToGbOptions) -> Result<(), Error> { match self.device { GameBoyDevice::Cpu => gb.cpu().set_state(&self.state, Some(self.format))?, GameBoyDevice::Ppu => gb.ppu().set_state(&self.state, Some(self.format))?, GameBoyDevice::Apu => gb.apu().set_state(&self.state, Some(self.format))?, GameBoyDevice::Dma => gb.dma().set_state(&self.state, Some(self.format))?, GameBoyDevice::Pad => gb.pad().set_state(&self.state, Some(self.format))?, GameBoyDevice::Timer => gb.timer().set_state(&self.state, Some(self.format))?, _ => return Err(Error::NotImplemented), } Ok(()) } } impl Serialize for BosDeviceState { fn write<W: Write + Seek>(&mut self, writer: &mut W) -> Result<(), Error> { self.header.write(writer)?; write_u8(writer, self.device as u8)?; write_u8(writer, self.format as u8)?; write_bytes(writer, &self.state)?; Ok(()) } fn read<R: Read + Seek>(&mut self, reader: &mut R) -> Result<(), Error> { self.header.read(reader)?; self.device = read_u8(reader)?.into(); self.format = read_u8(reader)?.into(); let state_len = self.header.size as usize - size_of::<u8>() - size_of::<u8>(); self.state.append(&mut read_bytes(reader, state_len)?); Ok(()) } } impl Default for BosDeviceState { fn default() -> Self { Self::new(GameBoyDevice::Unknown, StateFormat::Partial, vec![]) } } #[cfg_attr(feature = "wasm", wasm_bindgen)] #[derive(Default)] pub struct BessState { footer: BessFooter, name: BessName, info: BessInfo, core: BessCore, mbc: BessMbc, end: BessBlock, } impl BessState { /// Checks if the data contained in the provided /// buffer represents a valid BESS (Best Effort Save State) /// file structure, thought magic string validation. pub fn is_bess<R: Read + Seek>(reader: &mut R) -> Result<bool, Error> { reader.seek(SeekFrom::End(-4))?; let magic = read_u32(reader)?; reader.rewind()?; Ok(magic == BESS_MAGIC) } pub fn description(&self, column_length: usize) -> String { let emulator_l = format!("{:width$}", "Emulator", width = column_length); let title_l: String = format!("{:width$}", "Title", width = column_length); let version_l: String = format!("{:width$}", "Version", width = column_length); let model_l: String = format!("{:width$}", "Model", width = column_length); let ram_l: String = format!("{:width$}", "RAM", width = column_length); let vram_l: String = format!("{:width$}", "VRAM", width = column_length); let pc_l: String = format!("{:width$}", "PC", width = column_length); let sp_l: String = format!("{:width$}", "SP", width = column_length); format!( "{} {}\n{} {}\n{} {}.{}\n{} {}\n{} {}\n{} {}\n{} 0x{:04X}\n{} 0x{:04X}\n", emulator_l, self.name.name, title_l, self.info.title(), version_l, self.core.major, self.core.minor, model_l, self.core.model, ram_l, self.core.ram.size, vram_l, self.core.vram.size, pc_l, self.core.pc, sp_l, self.core.sp ) } pub fn verify(&self) -> Result<(), Error> { self.footer.verify()?; self.core.verify()?; Ok(()) } /// Dumps the core data into the provided buffer and returns. /// This will effectively populate the majority of the save /// file with the core emulator contents. fn dump_core<W: Write + Seek>(&mut self, writer: &mut W) -> Result<(), Error> { let mut buffers = [ &mut self.core.ram, &mut self.core.vram, &mut self.core.mbc_ram, &mut self.core.oam, &mut self.core.hram, &mut self.core.background_palettes, &mut self.core.object_palettes, ]; for item in buffers.iter_mut() { item.offset = writer.stream_position()? as u32; write_bytes(writer, &item.buffer)?; } Ok(()) } } impl StateInfo for BessState { fn timestamp(&self) -> Result<u64, Error> { Ok(0) } fn agent(&self) -> Result<String, Error> { Ok(self.name.name.clone()) } fn model(&self) -> Result<String, Error> { Ok(self.core.mode().into()) } fn title(&self) -> Result<String, Error> { Ok(self.info.title()) } fn image_eager(&self) -> Result<Vec<u8>, Error> { Err(Error::NotImplemented) } fn has_image(&self) -> bool { false } } #[cfg(feature = "wasm")] #[cfg_attr(feature = "wasm", wasm_bindgen)] impl BessState { pub fn timestamp_wa(&self) -> Result<u64, String> { Ok(Self::timestamp(self)?) } pub fn agent_wa(&self) -> Result<String, String> { Ok(Self::agent(self)?) } pub fn model_wa(&self) -> Result<String, String> { Ok(Self::model(self)?) } pub fn title_wa(&self) -> Result<String, String> { Ok(Self::title(self)?) } pub fn image_eager_wa(&self) -> Result<Vec<u8>, String> { Ok(Self::image_eager(self)?) } pub fn has_image_wa(&self) -> bool { self.has_image() } } impl Serialize for BessState { fn write<W: Write + Seek>(&mut self, writer: &mut W) -> Result<(), Error> { self.dump_core(writer)?; self.footer.start_offset = writer.stream_position()? as u32; self.name.write(writer)?; self.info.write(writer)?; self.core.write(writer)?; self.mbc.write(writer)?; self.end.write(writer)?; self.footer.write(writer)?; Ok(()) } fn read<R: Read + Seek>(&mut self, reader: &mut R) -> Result<(), Error> { // moves the cursor to the end of the file // to read the footer, and then places the // the cursor in the start of the BESS data // according to the footer information reader.seek(SeekFrom::End(-8))?; self.footer.read(reader)?; reader.seek(SeekFrom::Start(self.footer.start_offset as u64))?; loop { // reads the block header information and then moves the // cursor back to the original position to be able to // re-read the block data let block = BessBlockHeader::from_data(reader)?; let offset = -((size_of::<u32>() * 2) as i64); reader.seek(SeekFrom::Current(offset))?; match block.magic.as_str() { "NAME" => self.name = BessName::from_data(reader)?, "INFO" => self.info = BessInfo::from_data(reader)?, "CORE" => self.core = BessCore::from_data(reader)?, "MBC " => self.mbc = BessMbc::from_data(reader)?, "END " => self.end = BessBlock::from_data(reader)?, _ => { BessBlock::from_data(reader)?; } } if block.is_end() { break; } } Ok(()) } } impl StateBox for BessState { fn from_gb(gb: &mut GameBoy, options: &FromGbOptions) -> Result<Box<Self>, Error> { Ok(Box::new(Self { footer: BessFooter::default(), name: *<BessName as StateBox>::from_gb(gb, options)?, info: BessInfo::from_gb(gb)?, core: BessCore::from_gb(gb)?, mbc: BessMbc::from_gb(gb)?, end: BessBlock::from_magic(String::from("END ")), })) } fn to_gb(&self, gb: &mut GameBoy, options: &ToGbOptions) -> Result<(), Error> { self.verify()?; StateBox::to_gb(&self.name, gb, options)?; self.info.to_gb(gb)?; self.core.to_gb(gb)?; self.mbc.to_gb(gb)?; Ok(()) } } impl StateConfig for BessState { fn mode(&self) -> Result<GameBoyMode, Error> { match self.core.model.chars().next() { Some('G') => Ok(GameBoyMode::Dmg), Some('S') => Ok(GameBoyMode::Sgb), Some('C') => Ok(GameBoyMode::Cgb), None | Some(_) => Err(Error::InvalidData), } } } impl Display for BessState { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { write!(f, "{}", self.description(9)) } } pub struct BessBlockHeader { magic: String, size: u32, } impl BessBlockHeader { pub fn new(magic: String, size: u32) -> Self { Self { magic, size } } pub fn from_data<R: Read + Seek>(reader: &mut R) -> Result<Self, Error> { let mut instance = Self::default(); instance.read(reader)?; Ok(instance) } pub fn is_end(&self) -> bool { self.magic == "END " } } impl Serialize for BessBlockHeader { fn write<W: Write + Seek>(&mut self, writer: &mut W) -> Result<(), Error> { write_bytes(writer, self.magic.as_bytes())?; write_u32(writer, self.size)?; Ok(()) } fn read<R: Read + Seek>(&mut self, reader: &mut R) -> Result<(), Error> { self.magic = String::from_utf8(read_bytes(reader, 4)?)?; self.size = read_u32(reader)?; Ok(()) } } impl Default for BessBlockHeader { fn default() -> Self { Self::new(String::from(" "), 0) } } pub struct BessBlock { header: BessBlockHeader, buffer: Vec<u8>, } impl BessBlock { pub fn new(header: BessBlockHeader, buffer: Vec<u8>) -> Self { Self { header, buffer } } pub fn from_magic(magic: String) -> Self { Self::new(BessBlockHeader::new(magic, 0), vec![]) } pub fn from_data<R: Read + Seek>(reader: &mut R) -> Result<Self, Error> { let mut instance = Self::default(); instance.read(reader)?; Ok(instance) } pub fn magic(&self) -> &String { &self.header.magic } pub fn is_end(&self) -> bool { self.header.is_end() } } impl Serialize for BessBlock { fn write<W: Write + Seek>(&mut self, writer: &mut W) -> Result<(), Error> { self.header.write(writer)?; write_bytes(writer, &self.buffer)?; Ok(()) } fn read<R: Read + Seek>(&mut self, reader: &mut R) -> Result<(), Error> { self.header.read(reader)?; self.buffer.reserve_exact(self.header.size as usize); read_into(reader, &mut self.buffer)?; Ok(()) } } impl Default for BessBlock { fn default() -> Self { Self::new(BessBlockHeader::default(), vec![]) } } pub struct BessBuffer { size: u32, offset: u32, buffer: Vec<u8>, } impl BessBuffer { pub fn new(size: u32, offset: u32, buffer: Vec<u8>) -> Self { Self { size, offset, buffer, } } /// Fills the buffer with new data and updating the size /// value accordingly. fn fill_buffer(&mut self, data: &[u8]) { self.size = data.len() as u32; self.buffer = data.to_vec(); } /// Loads the internal buffer structure with the provided /// data according to the size and offset defined. fn load_buffer<R: Read + Seek>(&self, reader: &mut R) -> Result<Vec<u8>, Error> { let mut buffer = vec![0x00; self.size as usize]; let position = reader.stream_position()?; reader.seek(SeekFrom::Start(self.offset as u64))?; read_into(reader, &mut buffer)?; reader.seek(SeekFrom::Start(position))?; Ok(buffer) } } impl Serialize for BessBuffer { fn write<W: Write + Seek>(&mut self, writer: &mut W) -> Result<(), Error> { write_u32(writer, self.size)?; write_u32(writer, self.offset)?; Ok(()) } fn read<R: Read + Seek>(&mut self, reader: &mut R) -> Result<(), Error> { self.size = read_u32(reader)?; self.offset = read_u32(reader)?; self.buffer = self.load_buffer(reader)?; Ok(()) } } impl Default for BessBuffer { fn default() -> Self { Self::new(0, 0, vec![]) } } pub struct BessFooter { start_offset: u32, magic: u32, } impl BessFooter { pub fn new(start_offset: u32, magic: u32) -> Self { Self { start_offset, magic, } } pub fn verify(&self) -> Result<(), Error> { if self.magic != BESS_MAGIC { return Err(Error::DataError(String::from("Invalid magic"))); } Ok(()) } } impl Serialize for BessFooter { fn write<W: Write + Seek>(&mut self, writer: &mut W) -> Result<(), Error> { write_u32(writer, self.start_offset)?; write_u32(writer, self.magic)?; Ok(()) } fn read<R: Read + Seek>(&mut self, reader: &mut R) -> Result<(), Error> { self.start_offset = read_u32(reader)?; self.magic = read_u32(reader)?; Ok(()) } } impl Default for BessFooter { fn default() -> Self { Self::new(0x00, BESS_MAGIC) } } pub struct BessName { header: BessBlockHeader, name: String, } impl BessName { pub fn new(name: String) -> Self { Self { header: BessBlockHeader::new(String::from("NAME"), name.len() as u32), name, } } pub fn from_data<R: Read + Seek>(reader: &mut R) -> Result<Self, Error> { let mut instance = Self::default(); instance.read(reader)?; Ok(instance) } pub fn format_name(name: &str, version: &str) -> String { format!("{name} v{version}") } pub fn build_name(&mut self, name: &str, version: &str) { self.name = Self::format_name(name, version); } } impl Serialize for BessName { fn write<W: Write + Seek>(&mut self, writer: &mut W) -> Result<(), Error> { self.header.write(writer)?; write_bytes(writer, self.name.as_bytes())?; Ok(()) } fn read<R: Read + Seek>(&mut self, reader: &mut R) -> Result<(), Error> { self.header.read(reader)?; self.name = String::from_utf8(read_bytes(reader, self.header.size as usize)?)?; Ok(()) } } impl State for BessName { fn from_gb(_gb: &mut GameBoy) -> Result<Self, Error> { Ok(Self::new(Self::format_name( &Info::name(), &Info::version(), ))) } fn to_gb(&self, _gb: &mut GameBoy) -> Result<(), Error> { Ok(()) } } impl StateBox for BessName { fn from_gb(_gb: &mut GameBoy, options: &FromGbOptions) -> Result<Box<Self>, Error> where Self: Sized, { Ok(Box::new(Self::new(Self::format_name( &options.agent.clone().unwrap_or(Info::name()), &options.agent_version.clone().unwrap_or(Info::version()), )))) } fn to_gb(&self, _gb: &mut GameBoy, _options: &ToGbOptions) -> Result<(), Error> { Ok(()) } } impl Default for BessName { fn default() -> Self { Self::new(String::from("")) } } pub struct BessInfo { header: BessBlockHeader, title: [u8; 16], checksum: [u8; 2], } impl BessInfo { pub fn new(title: &[u8], checksum: &[u8]) -> Self { Self { header: BessBlockHeader::new( String::from("INFO"), title.len() as u32 + checksum.len() as u32, ), title: title.try_into().unwrap(), checksum: checksum.try_into().unwrap(), } } pub fn from_data<R: Read + Seek>(reader: &mut R) -> Result<Self, Error> { let mut instance = Self::default(); instance.read(reader)?; Ok(instance) } pub fn title(&self) -> String { let mut final_index = 16; for (offset, byte) in self.title.iter().enumerate() { if *byte == 0u8 { final_index = offset; break; } // in we're at the final byte of the title and the value // is one that is reserved for CGB compatibility testing // then we must ignore it for title processing purposes if offset > 14 && (*byte == CgbMode::CgbCompatible as u8 || *byte == CgbMode::CgbOnly as u8) { final_index = offset; break; } } String::from( String::from_utf8(Vec::from(&self.title[..final_index])) .unwrap() .trim_matches(char::from(0)) .trim(), ) } } impl Serialize for BessInfo { fn write<W: Write + Seek>(&mut self, writer: &mut W) -> Result<(), Error> { self.header.write(writer)?; write_bytes(writer, &self.title)?; write_bytes(writer, &self.checksum)?; Ok(()) } fn read<R: Read + Seek>(&mut self, reader: &mut R) -> Result<(), Error> { self.header.read(reader)?; read_into(reader, &mut self.title)?; read_into(reader, &mut self.checksum)?; Ok(()) } } impl State for BessInfo { fn from_gb(gb: &mut GameBoy) -> Result<Self, Error> { Ok(Self::new( &gb.cartridge_i().rom_data()[0x0134..=0x0143], &gb.cartridge_i().rom_data()[0x014e..=0x014f], )) } fn to_gb(&self, gb: &mut GameBoy) -> Result<(), Error> { if self.title() != gb.rom_i().title() { return Err(Error::DataError(format!( "Invalid ROM loaded, expected '{}' (len {}) got '{}' (len {})", self.title(), self.title().len(), gb.rom_i().title(), gb.rom_i().title().len(), ))); } Ok(()) } } impl Default for BessInfo { fn default() -> Self { Self::new(&[0_u8; 16], &[0_u8; 2]) } } pub struct BessCore { header: BessBlockHeader, major: u16, minor: u16, model: String, pc: u16, af: u16, bc: u16, de: u16, hl: u16, sp: u16, ime: bool, ie: u8, // 0 = running; 1 = halted; 2 = stopped execution_mode: u8, _padding: u8, io_registers: [u8; 128], ram: BessBuffer, vram: BessBuffer, mbc_ram: BessBuffer, oam: BessBuffer, hram: BessBuffer, background_palettes: BessBuffer, object_palettes: BessBuffer, } impl BessCore { #[allow(clippy::too_many_arguments)] pub fn new( model: String, pc: u16, af: u16, bc: u16, de: u16, hl: u16, sp: u16, ime: bool, ie: u8, execution_mode: u8, io_registers: [u8; 128], ) -> Self { Self { header: BessBlockHeader::new( String::from("CORE"), ((size_of::<u16>() * 2) + size_of::<u32>() + (size_of::<u16>() * 6) + (size_of::<u8>() * 4) + (size_of::<u8>() * 128) + ((size_of::<u32>() + size_of::<u32>()) * 7)) as u32, ), major: 1, minor: 1, model, pc, af, bc, de, hl, sp, ime, ie, execution_mode, _padding: 0, io_registers, ram: BessBuffer::default(), vram: BessBuffer::default(), mbc_ram: BessBuffer::default(), oam: BessBuffer::default(), hram: BessBuffer::default(), background_palettes: BessBuffer::default(), object_palettes: BessBuffer::default(), } } pub fn from_data<R: Read + Seek>(reader: &mut R) -> Result<Self, Error> { let mut instance = Self::default(); instance.read(reader)?; Ok(instance) } pub fn verify(&self) -> Result<(), Error> { if self.header.magic != "CORE" { return Err(Error::DataError(String::from("Invalid magic"))); } if self.major != 1 { return Err(Error::DataError(String::from("Invalid major version"))); } if self.minor != 1 { return Err(Error::DataError(String::from("Invalid minor version"))); } if self.oam.size != 0xa0 { return Err(Error::DataError(String::from("Invalid OAM size"))); } if self.hram.size != 0x7f { return Err(Error::DataError(String::from("Invalid HRAM size"))); } if (self.is_cgb() && self.background_palettes.size != 0x40) || (self.is_dmg() && self.background_palettes.size != 0x00) { return Err(Error::DataError(String::from( "Invalid background palettes size", ))); } if (self.is_cgb() && self.object_palettes.size != 0x40) || (self.is_dmg() && self.object_palettes.size != 0x00) { return Err(Error::DataError(String::from( "Invalid object palettes size", ))); } Ok(()) } pub fn mode(&self) -> GameBoyMode { if self.is_dmg() { return GameBoyMode::Dmg; } if self.is_cgb() { return GameBoyMode::Cgb; } if self.is_sgb() { return GameBoyMode::Sgb; } GameBoyMode::Dmg } /// Obtains the BESS (Game Boy) model string using the /// provided `GameBoy` instance. fn bess_model(gb: &GameBoy) -> String { let mut buffer = [0x00_u8; 4]; if gb.is_dmg() { buffer[0] = b'G'; } else if gb.is_cgb() { buffer[0] = b'C'; } else if gb.is_sgb() { buffer[0] = b'S'; } else { buffer[0] = b' '; } if gb.is_dmg() { buffer[1] = b'D'; } else if gb.is_cgb() { buffer[1] = b'C'; } else if gb.is_sgb() { buffer[1] = b'N'; } else { buffer[1] = b' '; } if gb.is_dmg() { buffer[2] = b'B'; } else if gb.is_cgb() { buffer[2] = b'A'; } else { buffer[2] = b' '; } buffer[3] = b' '; String::from_utf8(Vec::from(buffer)).unwrap() } fn is_dmg(&self) -> bool { if let Some(first_char) = self.model.chars().next() { return first_char == 'G'; } false } fn is_cgb(&self) -> bool { if let Some(first_char) = self.model.chars().next() { return first_char == 'C'; } false } fn is_sgb(&self) -> bool { if let Some(first_char) = self.model.chars().next() { return first_char == 'S'; } false } } impl Serialize for BessCore { fn write<W: Write + Seek>(&mut self, writer: &mut W) -> Result<(), Error> { self.header.write(writer)?; write_u16(writer, self.major)?; write_u16(writer, self.minor)?; write_bytes(writer, self.model.as_bytes())?; write_u16(writer, self.pc)?; write_u16(writer, self.af)?; write_u16(writer, self.bc)?; write_u16(writer, self.de)?; write_u16(writer, self.hl)?; write_u16(writer, self.sp)?; write_u8(writer, self.ime as u8)?; write_u8(writer, self.ie)?; write_u8(writer, self.execution_mode)?; write_u8(writer, self._padding)?; write_bytes(writer, &self.io_registers)?; self.ram.write(writer)?; self.vram.write(writer)?; self.mbc_ram.write(writer)?; self.oam.write(writer)?; self.hram.write(writer)?; self.background_palettes.write(writer)?; self.object_palettes.write(writer)?; Ok(()) } fn read<R: Read + Seek>(&mut self, reader: &mut R) -> Result<(), Error> { self.header.read(reader)?; self.major = read_u16(reader)?; self.minor = read_u16(reader)?; self.model = String::from_utf8(read_bytes(reader, 4)?)?; self.pc = read_u16(reader)?; self.af = read_u16(reader)?; self.bc = read_u16(reader)?; self.de = read_u16(reader)?; self.hl = read_u16(reader)?; self.sp = read_u16(reader)?; self.ime = read_u8(reader)? != 0; self.ie = read_u8(reader)?; self.execution_mode = read_u8(reader)?; self._padding = read_u8(reader)?; read_into(reader, &mut self.io_registers)?; self.ram.read(reader)?; self.vram.read(reader)?; self.mbc_ram.read(reader)?; self.oam.read(reader)?; self.hram.read(reader)?; self.background_palettes.read(reader)?; self.object_palettes.read(reader)?; Ok(()) } } impl State for BessCore { fn from_gb(gb: &mut GameBoy) -> Result<Self, Error> { let mut core = Self::new( Self::bess_model(gb), gb.cpu_i().pc(), gb.cpu_i().af(), gb.cpu_i().bc(), gb.cpu_i().de(), gb.cpu_i().hl(), gb.cpu_i().sp(), gb.cpu_i().ime(), gb.mmu_i().ie, u8::from(gb.cpu().halted()), { // @TODO: These registers cannot be completely retrieved // and because of that some audio noise is played when loading state. // The loading of the registers should be done in a much // more manual way like SameBoy does here: // https://github.com/LIJI32/SameBoy/blob/7e6f1f866e89430adaa6be839aecc4a2ccabd69c/Core/save_state.c#L673 disable_pedantic!(); let io_registers = gb.mmu().read_many_raw(0xff00, 128).try_into().unwrap(); enable_pedantic!(); io_registers }, ); core.ram.fill_buffer(gb.mmu().ram()); core.vram.fill_buffer(gb.ppu().vram_device()); core.mbc_ram.fill_buffer(gb.rom_i().ram_data()); core.oam.fill_buffer(&gb.mmu().read_many(0xfe00, 0x00a0)); core.hram.fill_buffer(&gb.mmu().read_many(0xff80, 0x007f)); if gb.is_cgb() { core.background_palettes .fill_buffer(&gb.ppu_i().palettes_color()[0]); core.object_palettes .fill_buffer(&gb.ppu_i().palettes_color()[1]); } Ok(core) } fn to_gb(&self, gb: &mut GameBoy) -> Result<(), Error> { gb.cpu().set_pc(self.pc); gb.cpu().set_af(self.af); gb.cpu().set_bc(self.bc); gb.cpu().set_de(self.de); gb.cpu().set_hl(self.hl); gb.cpu().set_sp(self.sp); gb.cpu().set_ime(self.ime); gb.mmu().ie = self.ie; match self.execution_mode { 0 => gb.cpu().set_halted(false), 1 => gb.cpu().set_halted(true), 2 => gb.cpu().stop(), _ => unimplemented!(), } // @TODO: we need to be careful about this writing and // should make this a bit more robust, to handle this // special case/situations // The registers should be handled in a more manual manner // to avoid unwanted side effects // https://github.com/LIJI32/SameBoy/blob/7e6f1f866e89430adaa6be839aecc4a2ccabd69c/Core/save_state.c#L1003 disable_pedantic!(); gb.mmu().write_many(0xff00, &self.io_registers); enable_pedantic!(); gb.mmu().set_ram(self.ram.buffer.to_vec()); gb.ppu().set_vram(&self.vram.buffer); gb.ppu().set_oam(&self.oam.buffer); gb.ppu().set_hram(&self.hram.buffer); gb.rom().set_ram_data(&self.mbc_ram.buffer); // disables a series of operations that would otherwise be // triggered by the writing of associated registers gb.dma().set_active_dma(false); gb.serial().set_transferring(false); // clears the PPU screen resetting the mode cycle clock // and other PPU cycle control structures gb.ppu().clear_screen(false); if gb.is_cgb() { // updates the internal palettes for the CGB with the values // stored in the BESS state gb.ppu().set_palettes_color([ self.background_palettes.buffer.to_vec().try_into().unwrap(), self.object_palettes.buffer.to_vec().try_into().unwrap(), ]); // updates the speed of the CGB according to the KEY1 register let is_double = self.io_registers[0x4d_usize] & 0x80 == 0x80; gb.mmu().set_speed(if is_double { GameBoySpeed::Double } else { GameBoySpeed::Normal }); // need to disable HDMA transfer to avoid unwanted // DMA transfers when loading the state gb.dma().set_active_hdma(false); } Ok(()) } } impl Default for BessCore { fn default() -> Self { Self::new( String::from("GD "), 0x0000_u16, 0x0000_u16, 0x0000_u16, 0x0000_u16, 0x0000_u16, 0x0000_u16, false, 0x00, 0, [0x00; 128], ) } } pub struct BessMbrRegister { address: u16, value: u8, } impl BessMbrRegister { pub fn new(address: u16, value: u8) -> Self { Self { address, value } } } pub struct BessMbc { header: BessBlockHeader, registers: Vec<BessMbrRegister>, } impl BessMbc { pub fn new(registers: Vec<BessMbrRegister>) -> Self { Self { header: BessBlockHeader::new( String::from("MBC "), ((size_of::<u8>() + size_of::<u16>()) * registers.len()) as u32, ), registers, } } pub fn from_data<R: Read + Seek>(reader: &mut R) -> Result<Self, Error> { let mut instance = Self::default(); instance.read(reader)?; Ok(instance) } } impl Serialize for BessMbc { fn write<W: Write + Seek>(&mut self, writer: &mut W) -> Result<(), Error> { self.header.write(writer)?; for register in self.registers.iter() { write_u16(writer, register.address)?; write_u8(writer, register.value)?; } Ok(()) } fn read<R: Read + Seek>(&mut self, reader: &mut R) -> Result<(), Error> { self.header.read(reader)?; for _ in 0..(self.header.size / 3) { let address = read_u16(reader)?; let value = read_u8(reader)?; self.registers.push(BessMbrRegister::new(address, value)); } Ok(()) } } impl State for BessMbc { fn from_gb(gb: &mut GameBoy) -> Result<Self, Error> { let mut registers = vec![]; match gb.cartridge().rom_type().mbc_type() { MbcType::NoMbc => (), MbcType::Mbc1 => { registers.push(BessMbrRegister::new( 0x0000, if gb.rom().ram_enabled() { 0x0a_u8 } else { 0x00_u8 }, )); registers.push(BessMbrRegister::new( 0x2000, gb.rom().rom_bank() as u8 & 0x1f, )); registers.push(BessMbrRegister::new(0x4000, gb.rom().ram_bank())); registers.push(BessMbrRegister::new(0x6000, 0x00_u8)); } MbcType::Mbc3 => { registers.push(BessMbrRegister::new( 0x0000, if gb.rom().ram_enabled() { 0x0a_u8 } else { 0x00_u8 }, )); registers.push(BessMbrRegister::new(0x2000, gb.rom().rom_bank() as u8)); registers.push(BessMbrRegister::new(0x4000, gb.rom().ram_bank())); } MbcType::Mbc5 => { registers.push(BessMbrRegister::new( 0x0000, if gb.rom().ram_enabled() { 0x0a_u8 } else { 0x00_u8 }, )); registers.push(BessMbrRegister::new(0x2000, gb.rom().rom_bank() as u8)); registers.push(BessMbrRegister::new( 0x3000, (gb.rom().rom_bank() >> 8) as u8 & 0x01, )); registers.push(BessMbrRegister::new(0x4000, gb.rom().ram_bank())); } _ => unimplemented!(), } Ok(Self::new(registers)) } fn to_gb(&self, gb: &mut GameBoy) -> Result<(), Error> { for register in self.registers.iter() { gb.mmu().write(register.address, register.value); } Ok(()) } } impl Default for BessMbc { fn default() -> Self { Self::new(vec![]) } } /// Top level manager structure containing the /// entrypoint static methods for saving and loading /// [BESS](https://github.com/LIJI32/SameBoy/blob/master/BESS.md) state /// files and buffers for the Game Boy. #[cfg_attr(feature = "wasm", wasm_bindgen)] pub struct StateManager; impl StateManager { pub fn save_file( file_path: &str, gb: &mut GameBoy, format: Option<SaveStateFormat>, options: Option<FromGbOptions>, ) -> Result<(), Error> { let mut file = File::create(file_path) .map_err(|_| Error::IoError(format!("Failed to create file: {file_path}")))?; let data = Self::save(gb, format, options)?; file.write_all(&data) .map_err(|_| Error::IoError(format!("Failed to write to file: {file_path}")))?; file.flush() .map_err(|_| Error::IoError(format!("Failed to flush file: {file_path}")))?; Ok(()) } pub fn load_file( file_path: &str, gb: &mut GameBoy, format: Option<SaveStateFormat>, options: Option<ToGbOptions>, ) -> Result<(), Error> { let mut file = File::open(file_path) .map_err(|_| Error::IoError(format!("Failed to open file: {file_path}")))?; let mut data = vec![]; file.read_to_end(&mut data) .map_err(|_| Error::IoError(format!("Failed to read from file: {file_path}")))?; Self::load(&data, gb, format, options)?; Ok(()) } } impl StateManager { pub fn save( gb: &mut GameBoy, format: Option<SaveStateFormat>, options: Option<FromGbOptions>, ) -> Result<Vec<u8>, Error> { let options = options.unwrap_or_default(); let mut data = Cursor::new(vec![]); match format { Some(SaveStateFormat::Bosc) | None => { let mut state = BoscState::from_gb(gb, &options)?; state.write(&mut data)?; } Some(SaveStateFormat::Bos) => { let mut state = BosState::from_gb(gb, &options)?; state.write(&mut data)?; } Some(SaveStateFormat::Bess) => { let mut state = BessState::from_gb(gb, &options)?; state.write(&mut data)?; } } Ok(data.into_inner()) } pub fn load( data: &[u8], gb: &mut GameBoy, format: Option<SaveStateFormat>, options: Option<ToGbOptions>, ) -> Result<(), Error> { let options = options.unwrap_or_default(); let data = &mut Cursor::new(data.to_vec()); let format = match format { Some(format) => format, None => { if BoscState::is_bosc(data)? { SaveStateFormat::Bosc } else if BosState::is_bos(data)? { SaveStateFormat::Bos } else if BessState::is_bess(data)? { SaveStateFormat::Bess } else { return Err(Error::InvalidParameter(String::from( "Unknown save state file format", ))); } } }; match format { SaveStateFormat::Bosc => { let mut state = BoscState::default(); Self::load_inner(&mut state, data, gb, &options)?; } SaveStateFormat::Bos => { let mut state = BosState::default(); Self::load_inner(&mut state, data, gb, &options)?; } SaveStateFormat::Bess => { let mut state = BessState::default(); Self::load_inner(&mut state, data, gb, &options)?; } } Ok(()) } pub fn read_bos_auto(data: &[u8]) -> Result<BosState, Error> { match Self::format(data)? { SaveStateFormat::Bosc => { let mut state = BoscState::default(); let data = &mut Cursor::new(data.to_vec()); state.read(data)?; Ok(state.bos) } SaveStateFormat::Bos => { let mut state = BosState::default(); let data = &mut Cursor::new(data.to_vec()); state.read(data)?; Ok(state) } SaveStateFormat::Bess => Err(Error::InvalidParameter(String::from( "Incompatible save state file format (BESS)", ))), } } pub fn read_bosc(data: &[u8]) -> Result<BoscState, Error> { let data = &mut Cursor::new(data.to_vec()); let mut state = BoscState::default(); state.read(data)?; Ok(state) } pub fn read_bos(data: &[u8]) -> Result<BosState, Error> { let data = &mut Cursor::new(data.to_vec()); let mut state = BosState::default(); state.read(data)?; Ok(state) } pub fn read_bess(data: &[u8]) -> Result<BessState, Error> { let data = &mut Cursor::new(data.to_vec()); let mut state = BessState::default(); state.read(data)?; Ok(state) } pub fn format(data: &[u8]) -> Result<SaveStateFormat, Error> { let data = &mut Cursor::new(data.to_vec()); if BoscState::is_bosc(data)? { Ok(SaveStateFormat::Bosc) } else if BosState::is_bos(data)? { Ok(SaveStateFormat::Bos) } else if BessState::is_bess(data)? { Ok(SaveStateFormat::Bess) } else { Err(Error::InvalidData) } } /// Validates the provided state data and runs a series of simple /// validations according to the provided params. pub fn validate(data: &[u8], title: Option<String>) -> Result<(), Error> { match Self::format(data)? { SaveStateFormat::Bosc | SaveStateFormat::Bos => { let state = Self::read_bos_auto(data)?; if let Some(title) = title { if state.title()? != title { return Err(Error::InvalidData); } } } SaveStateFormat::Bess => { let state = Self::read_bess(data)?; if let Some(title) = title { if state.title()? != title { return Err(Error::InvalidData); } } } } Ok(()) } /// Obtains the thumbnail of the save state file, this thumbnail is /// stored in raw RGB format. /// /// This operation is currently only supported for the BOS format. pub fn thumbnail(data: &[u8], format: Option<SaveStateFormat>) -> Result<Vec<u8>, Error> { let format = match format { Some(format) => format, None => Self::format(data)?, }; match format { SaveStateFormat::Bosc => { let mut state = BoscState::default(); let data = &mut Cursor::new(data.to_vec()); state.read(data)?; Ok(state .bos .image_buffer .ok_or(Error::InvalidData)? .image .to_vec()) } SaveStateFormat::Bos => { let mut state = BosState::default(); let data = &mut Cursor::new(data.to_vec()); state.read(data)?; Ok(state.image_buffer.ok_or(Error::InvalidData)?.image.to_vec()) } SaveStateFormat::Bess => Err(Error::InvalidParameter(String::from( "Format foes not support thumbnail", ))), } } fn load_inner<T: Serialize + StateBox + StateConfig + Default, R: Read + Seek>( state: &mut T, reader: &mut R, gb: &mut GameBoy, options: &ToGbOptions, ) -> Result<(), Error> { state.read(reader)?; // in case the hardware model in the (saved) state is // different from the current hardware model, we need // to set the hardware model if state.mode()? != gb.mode() { gb.set_mode(state.mode()?); } // reload the Game Boy machine to make sure we're in // a clean state before loading the state if options.reload { gb.reload(); } state.to_gb(gb, options)?; Ok(()) } } #[cfg(feature = "wasm")] #[cfg_attr(feature = "wasm", wasm_bindgen)] impl StateManager { pub fn save_wa( gb: &mut GameBoy, format: Option<SaveStateFormat>, options: Option<FromGbOptions>, ) -> Result<Vec<u8>, String> { Ok(Self::save(gb, format, options)?) } pub fn load_wa( data: &[u8], gb: &mut GameBoy, format: Option<SaveStateFormat>, options: Option<ToGbOptions>, ) -> Result<(), String> { Ok(Self::load(data, gb, format, options)?) } pub fn read_bos_auto_wa(data: &[u8]) -> Result<BosState, String> { Ok(Self::read_bos_auto(data)?) } pub fn read_bosc_wa(data: &[u8]) -> Result<BoscState, String> { Ok(Self::read_bosc(data)?) } pub fn read_bos_wa(data: &[u8]) -> Result<BosState, String> { Ok(Self::read_bos(data)?) } pub fn read_bess_wa(data: &[u8]) -> Result<BessState, String> { Ok(Self::read_bess(data)?) } pub fn format_wa(data: &[u8]) -> Result<SaveStateFormat, String> { Ok(Self::format(data)?) } pub fn format_str_wa(data: &[u8]) -> Result<String, String> { Ok(Self::format(data)?.to_string()) } pub fn validate_wa(data: &[u8], title: Option<String>) -> Result<(), String> { Ok(Self::validate(data, title)?) } pub fn thumbnail_wa(data: &[u8], format: Option<SaveStateFormat>) -> Result<Vec<u8>, String> { Ok(Self::thumbnail(data, format)?) } } #[cfg(test)] mod tests { use boytacean_encoding::zippy::{decode_zippy, encode_zippy}; use crate::{ gb::GameBoy, state::{FromGbOptions, State}, }; use super::{BessCore, SaveStateFormat, StateManager}; #[test] fn test_bess_core() { let mut gb = GameBoy::default(); gb.load(true).unwrap(); gb.load_rom_file("res/roms/test/firstwhite.gb", None) .unwrap(); let bess_core = BessCore::from_gb(&mut gb).unwrap(); assert_eq!(bess_core.model, "GDB "); assert_eq!(bess_core.pc, 0x0000); assert_eq!(bess_core.af, 0x0000); assert_eq!(bess_core.bc, 0x0000); assert_eq!(bess_core.de, 0x0000); assert_eq!(bess_core.hl, 0x0000); assert_eq!(bess_core.sp, 0x0000); assert!(!bess_core.ime); assert_eq!(bess_core.ie, 0x00); assert_eq!(bess_core.execution_mode, 0); assert_eq!(bess_core.io_registers.len(), 128); assert_eq!( bess_core.io_registers, [ 63, 0, 0, 255, 0, 0, 0, 248, 255, 255, 255, 255, 255, 255, 255, 224, 128, 0, 0, 255, 56, 255, 0, 0, 255, 56, 127, 255, 159, 255, 56, 255, 0, 0, 0, 63, 0, 0, 240, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 134, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 126, 255, 254, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 255, 255, 255, 255, 249, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 ] ); assert_eq!(bess_core.ram.size, 0x2000); assert_eq!(bess_core.vram.size, 0x2000); assert_eq!(bess_core.mbc_ram.size, 0x2000); assert_eq!(bess_core.oam.size, 0x00a0); assert_eq!(bess_core.hram.size, 0x007f); assert_eq!(bess_core.background_palettes.size, 0x0000); assert_eq!(bess_core.object_palettes.size, 0x0000); } #[test] fn test_load_bosc() { let mut gb = GameBoy::default(); gb.load(true).unwrap(); gb.load_rom_file("res/roms/test/firstwhite.gb", None) .unwrap(); let data = StateManager::save(&mut gb, Some(SaveStateFormat::Bosc), None).unwrap(); StateManager::load(&data, &mut gb, Some(SaveStateFormat::Bosc), None).unwrap(); StateManager::load(&data, &mut gb, None, None).unwrap(); } #[test] fn test_load_bos() { let mut gb = GameBoy::default(); gb.load(true).unwrap(); gb.load_rom_file("res/roms/test/firstwhite.gb", None) .unwrap(); let data = StateManager::save(&mut gb, Some(SaveStateFormat::Bos), None).unwrap(); StateManager::load(&data, &mut gb, Some(SaveStateFormat::Bos), None).unwrap(); StateManager::load(&data, &mut gb, None, None).unwrap(); } #[test] fn test_load_bess() { let mut gb = GameBoy::default(); gb.load(true).unwrap(); gb.load_rom_file("res/roms/test/firstwhite.gb", None) .unwrap(); let data = StateManager::save(&mut gb, Some(SaveStateFormat::Bess), None).unwrap(); StateManager::load(&data, &mut gb, Some(SaveStateFormat::Bess), None).unwrap(); StateManager::load(&data, &mut gb, None, None).unwrap(); } #[test] fn test_bos_agent_version() { let mut gb = GameBoy::default(); gb.load(true).unwrap(); gb.load_rom_file("res/roms/test/firstwhite.gb", None) .unwrap(); let data = StateManager::save( &mut gb, Some(SaveStateFormat::Bos), Some(FromGbOptions { agent: Some(String::from("test-agent")), agent_version: Some(String::from("1.2.3")), ..Default::default() }), ) .unwrap(); let loaded_state = StateManager::read_bos(&data).unwrap(); let info = loaded_state.info.unwrap(); assert_eq!(info.agent, "test-agent"); assert_eq!(info.agent_version, "1.2.3"); } #[test] fn test_bess_agent_version() { let mut gb = GameBoy::default(); gb.load(true).unwrap(); gb.load_rom_file("res/roms/test/firstwhite.gb", None) .unwrap(); let data = StateManager::save( &mut gb, Some(SaveStateFormat::Bess), Some(FromGbOptions { agent: Some(String::from("TestAgent")), agent_version: Some(String::from("1.2.3")), ..Default::default() }), ) .unwrap(); let loaded_state = StateManager::read_bess(&data).unwrap(); assert_eq!(loaded_state.name.name, "TestAgent v1.2.3"); } #[test] fn test_compression() { let mut gb = GameBoy::default(); gb.load(true).unwrap(); gb.load_rom_file("res/roms/test/firstwhite.gb", None) .unwrap(); gb.step_to(0x0100); let data = StateManager::save( &mut gb, Some(SaveStateFormat::Bess), Some(FromGbOptions { agent_version: Some(String::from("0.0.0")), ..Default::default() }), ) .unwrap(); let encoded = encode_zippy(&data, None, None).unwrap(); let decoded = decode_zippy(&encoded, None).unwrap(); assert_eq!(data, decoded); assert_eq!(encoded.len(), 841); assert_eq!(decoded.len(), 25153); } }