1use crate::config::{CommonConfig, GenesisConfig}; 2use crate::extensions::Console; 3use crate::mainloop::create::{CreatableEmulator, ReadInputResult, WindowTitle}; 4use crate::mainloop::{CreatedEmulator, NativeDebugFn, NativeEmulatorError, create}; 5use crate::{NativeEmulator, NativeEmulatorResult, extensions}; 6use cdrom::reader::CdRom; 7use cdrom::reader::CdRomFileFormat; 8use genesis_config::{GenesisController, GenesisInputs, GenesisRegion}; 9use genesis_core::GenesisEmulator; 10use genesis_core::api::GenesisHardware; 11use jgenesis_common::frontend::{EmulatorTrait, SaveWriter}; 12use jgenesis_native_config::common::WindowSize; 13use jgenesis_native_config::input::mappings::ButtonMappingVec; 14use segacd_core::api::SegaCdLoadError; 15use std::fs; 16use std::path::{Path, PathBuf}; 17 18pub type NativeGenesisEmulator = NativeEmulator<GenesisEmulator>; 19 20#[derive(Clone)] 21pub struct GenesisCreateInput { 22 cartridge_rom: Option<Vec<u8>>, 23 sega_cd_bios_rom: Option<Vec<u8>>, 24 disc_path: Option<PathBuf>, 25} 26 27impl CreatableEmulator for GenesisEmulator { 28 type NativeConfig = GenesisConfig; 29 type CreateInput = GenesisCreateInput; 30 31 fn read_create_input( 32 config: &Self::NativeConfig, 33 ) -> NativeEmulatorResult<ReadInputResult<Self::CreateInput>> { 34 let read_cartridge_input = 35 || create::read_rom_file(&config.common.rom_file_path, &extensions::GENESIS_32X); 36 37 let mut rom_path = config.common.rom_file_path.clone(); 38 39 let sega_cd_bios_rom = if config.hardware.has_sega_cd() { 40 let (region, bios_path) = determine_scd_bios_path(config); 41 let Some(bios_path) = bios_path else { 42 return Err(NativeEmulatorError::SegaCdNoBios(region)); 43 }; 44 45 if config.scd_run_without_disc { 46 rom_path.clone_from(&bios_path); 47 } 48 49 let bios_rom = fs::read(&bios_path).map_err(|source| { 50 NativeEmulatorError::SegaCdBiosRead { path: bios_path, source } 51 })?; 52 Some(bios_rom) 53 } else { 54 None 55 }; 56 57 let (cartridge_input, disc_path) = if config.hardware.has_sega_cd() { 58 if config.scd_run_without_disc { 59 (None, None) 60 } else { 61 let primary_path_is_disc = 62 CdRomFileFormat::from_file_path(&config.common.rom_file_path).is_some(); 63 if primary_path_is_disc { 64 (None, Some(config.common.rom_file_path.clone())) 65 } else { 66 // Assume primary path is a cartridge ROM image 67 let cartridge_input = read_cartridge_input()?; 68 (Some(cartridge_input), config.secondary_path.clone()) 69 } 70 } 71 } else { 72 let cartridge_input = read_cartridge_input()?; 73 (Some(cartridge_input), None) 74 }; 75 76 let save_extension = match &cartridge_input { 77 Some(input) => input.save_extension.clone(), 78 None => Console::SegaCd.standard_extension().to_owned(), 79 }; 80 81 Ok(ReadInputResult { 82 input: GenesisCreateInput { 83 cartridge_rom: cartridge_input.map(|input| input.input), 84 sega_cd_bios_rom, 85 disc_path, 86 }, 87 rom_path, 88 save_extension, 89 }) 90 } 91 92 fn create( 93 input: ReadInputResult<Self::CreateInput>, 94 config: &Self::NativeConfig, 95 save_writer: &mut impl SaveWriter, 96 ) -> NativeEmulatorResult<CreatedEmulator<Self>> { 97 let disc = match &input.input.disc_path { 98 Some(path) => { 99 Some(read_sega_cd_disc(path, config.emulator_config.sega_cd.load_disc_into_ram)?) 100 } 101 None => None, 102 }; 103 104 let mut emulator = GenesisEmulator::create( 105 config.hardware, 106 input.input.cartridge_rom, 107 input.input.sega_cd_bios_rom, 108 disc, 109 config.emulator_config.clone(), 110 save_writer, 111 )?; 112 113 let default_window_size = if config.hardware.has_32x() { 114 WindowSize::new_32x( 115 config.common.initial_window_size, 116 emulator.timing_mode(), 117 &config.emulator_config, 118 ) 119 } else { 120 WindowSize::new_genesis( 121 config.common.initial_window_size, 122 emulator.timing_mode(), 123 &config.emulator_config, 124 ) 125 }; 126 127 let window_title = generate_window_title(&mut emulator, config.hardware); 128 129 Ok(CreatedEmulator { emulator, window_title, default_window_size }) 130 } 131 132 fn common_config(config: &Self::NativeConfig) -> &CommonConfig { 133 &config.common 134 } 135 136 fn emulator_config(config: &Self::NativeConfig) -> &Self::Config { 137 &config.emulator_config 138 } 139 140 fn reload_native_config( 141 emulator: &mut NativeEmulator<Self>, 142 config: &Self::NativeConfig, 143 ) -> NativeEmulatorResult<()> { 144 update_controller_types(config, &mut emulator.inputs); 145 146 Ok(()) 147 } 148 149 fn initial_inputs(config: &Self::NativeConfig) -> Self::Inputs { 150 new_initial_inputs(config) 151 } 152 153 fn input_mappings(config: &Self::NativeConfig) -> ButtonMappingVec<'_, Self::Button> { 154 config.inputs.to_mapping_vec() 155 } 156 157 fn turbo_input_mappings(config: &Self::NativeConfig) -> ButtonMappingVec<'_, Self::Button> { 158 config.inputs.to_turbo_mapping_vec() 159 } 160 161 fn change_disc( 162 &mut self, 163 disc_path: &Path, 164 config: &<Self as EmulatorTrait>::Config, 165 ) -> NativeEmulatorResult<Option<WindowTitle>> { 166 let disc = read_sega_cd_disc(disc_path, config.sega_cd.load_disc_into_ram)?; 167 168 log::info!("Changing to disc read from path '{}'", disc_path.display()); 169 170 GenesisEmulator::change_disc(self, disc)?; 171 172 let window_title = generate_window_title(self, self.hardware()); 173 Ok(Some(WindowTitle(window_title))) 174 } 175 176 fn remove_disc(&mut self) -> Option<WindowTitle> { 177 GenesisEmulator::remove_disc(self); 178 179 let window_title = generate_window_title(self, self.hardware()); 180 Some(WindowTitle(window_title)) 181 } 182 183 fn debug_fn() -> Option<NativeDebugFn<Self>> { 184 Some(jgenesis_debugger_frontend::genesis::genesis_debug_fn) 185 } 186} 187 188fn new_initial_inputs(config: &GenesisConfig) -> GenesisInputs { 189 GenesisInputs { 190 p1: GenesisController::new(config.inputs.p1_type), 191 p2: GenesisController::new(config.inputs.p2_type), 192 } 193} 194 195fn update_controller_types(config: &GenesisConfig, inputs: &mut GenesisInputs) { 196 if config.inputs.p1_type != inputs.p1.controller_type() { 197 inputs.p1 = GenesisController::new(config.inputs.p1_type); 198 } 199 200 if config.inputs.p2_type != inputs.p2.controller_type() { 201 inputs.p2 = GenesisController::new(config.inputs.p2_type); 202 } 203} 204 205fn generate_window_title(emulator: &mut GenesisEmulator, hardware: GenesisHardware) -> String { 206 let system_name = hardware.to_string().to_ascii_lowercase(); 207 let mut game_title = emulator.game_title().unwrap_or("(no disc)".into()); 208 // Remove non-printable characters 209 game_title.retain(|c| { 210 c.is_ascii_alphanumeric() || c.is_ascii_whitespace() || c.is_ascii_punctuation() 211 }); 212 format!("{system_name} - {game_title}") 213} 214 215fn read_sega_cd_disc(path: &Path, open_in_memory: bool) -> NativeEmulatorResult<CdRom> { 216 let disc_format = CdRomFileFormat::from_file_path(path).unwrap_or_else(|| { 217 log::warn!("Unable to determine CD-ROM image format; assuming CUE/BIN"); 218 CdRomFileFormat::CueBin 219 }); 220 221 let disc = if open_in_memory { 222 CdRom::open_in_memory(path, disc_format) 223 } else { 224 CdRom::open(path, disc_format) 225 }; 226 227 disc.map_err(|err| NativeEmulatorError::SegaCdDisc(SegaCdLoadError::CdRom(err))) 228} 229 230fn determine_scd_bios_path(config: &GenesisConfig) -> (GenesisRegion, Option<PathBuf>) { 231 if !config.scd_per_region_bios { 232 return (GenesisRegion::Americas, bios_path_for_region(config, GenesisRegion::Americas)); 233 } 234 235 if let Some(region) = config.emulator_config.forced_region { 236 return (region, bios_path_for_region(config, region)); 237 } 238 239 let file_path = &config.common.rom_file_path; 240 let region = CdRomFileFormat::from_file_path(file_path) 241 .and_then(|cdrom_format| CdRom::open(file_path, cdrom_format).ok()) 242 .and_then(|mut disc| { 243 segacd_core::parse_disc_region(&mut disc).ok() 244 }) 245 .unwrap_or_else(|| { 246 log::error!("Unable to determine region of disc at '{}' for purposes of selecting BIOS path; defaulting to US", file_path.display()); 247 GenesisRegion::Americas 248 }); 249 250 (region, bios_path_for_region(config, region)) 251} 252 253fn bios_path_for_region(config: &GenesisConfig, region: GenesisRegion) -> Option<PathBuf> { 254 match region { 255 GenesisRegion::Americas => config.scd_us_bios_path.clone(), 256 GenesisRegion::Europe => config.scd_eu_bios_path.clone(), 257 GenesisRegion::Japan => config.scd_jp_bios_path.clone(), 258 } 259}