diff --git a/Cargo.toml b/Cargo.toml index f26dade..0866b3e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,4 +38,5 @@ gcn = ["flags"] genesis = ["flags", "signature"] ps1 = ["flags", "signature"] ps2 = ["flags", "signature"] +sms = ["flags", "signature"] wii = ["flags"] diff --git a/src/emulator/mod.rs b/src/emulator/mod.rs index adb03e8..c41b186 100644 --- a/src/emulator/mod.rs +++ b/src/emulator/mod.rs @@ -10,5 +10,7 @@ pub mod genesis; pub mod ps1; #[cfg(feature = "ps2")] pub mod ps2; +#[cfg(feature = "sms")] +pub mod sms; #[cfg(feature = "wii")] pub mod wii; diff --git a/src/emulator/sms/blastem.rs b/src/emulator/sms/blastem.rs new file mode 100644 index 0000000..5a00aef --- /dev/null +++ b/src/emulator/sms/blastem.rs @@ -0,0 +1,33 @@ +use crate::{runtime::MemoryRangeFlags, signature::Signature, Address, Address32, Process}; + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct State; + +impl State { + pub fn find_ram(&self, game: &Process) -> Option
{ + const SIG: Signature<15> = Signature::new("66 81 E1 FF 1F 0F B7 C9 8A 89 ?? ?? ?? ?? C3"); + + let scanned_address = game + .memory_ranges() + .filter(|m| { + m.flags() + .unwrap_or_default() + .contains(MemoryRangeFlags::WRITE) + && m.size().unwrap_or_default() == 0x101000 + }) + .find_map(|m| SIG.scan_process_range(game, m.range().ok()?))? + + 10; + + let wram: Address = game.read::(scanned_address).ok()?.into(); + + if wram.is_null() { + None + } else { + Some(wram) + } + } + + pub const fn keep_alive(&self) -> bool { + true + } +} diff --git a/src/emulator/sms/fusion.rs b/src/emulator/sms/fusion.rs new file mode 100644 index 0000000..da4b7c0 --- /dev/null +++ b/src/emulator/sms/fusion.rs @@ -0,0 +1,37 @@ +use crate::{signature::Signature, Address, Address32, Process}; + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct State { + addr: Address, +} + +impl State { + pub fn find_ram(&mut self, game: &Process) -> Option
{ + const SIG: Signature<4> = Signature::new("74 C8 83 3D"); + + let main_module = super::PROCESS_NAMES + .iter() + .filter(|(_, state)| matches!(state, super::State::Fusion(_))) + .find_map(|(name, _)| game.get_module_range(name).ok())?; + + let ptr = SIG.scan_process_range(game, main_module)? + 4; + self.addr = game.read::(ptr).ok()?.into(); + + Some(game.read::(self.addr).ok()?.add(0xC000).into()) + } + + pub fn keep_alive(&self, game: &Process, wram_base: &mut Option
) -> bool { + *wram_base = Some(match game.read::(self.addr) { + Ok(Address32::NULL) => Address::NULL, + Ok(x) => x.add(0xC000).into(), + _ => return false, + }); + true + } + + pub const fn new() -> Self { + Self { + addr: Address::NULL, + } + } +} diff --git a/src/emulator/sms/mednafen.rs b/src/emulator/sms/mednafen.rs new file mode 100644 index 0000000..569359d --- /dev/null +++ b/src/emulator/sms/mednafen.rs @@ -0,0 +1,30 @@ +use crate::{file_format::pe, signature::Signature, Address, Address32, Process}; + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct State; + +impl State { + pub fn find_ram(&self, game: &Process) -> Option
{ + const SIG_32: Signature<8> = Signature::new("25 FF 1F 00 00 0F B6 80"); + const SIG_64: Signature<7> = Signature::new("25 FF 1F 00 00 88 90"); + + let main_module_range = super::PROCESS_NAMES + .iter() + .filter(|(_, state)| matches!(state, super::State::Mednafen(_))) + .find_map(|(name, _)| game.get_module_range(name).ok())?; + + let is_64_bit = + pe::MachineType::read(game, main_module_range.0) == Some(pe::MachineType::X86_64); + + let ptr = match is_64_bit { + true => SIG_64.scan_process_range(game, main_module_range)? + 8, + false => SIG_32.scan_process_range(game, main_module_range)? + 7, + }; + + Some(game.read::(ptr).ok()?.into()) + } + + pub const fn keep_alive(&self) -> bool { + true + } +} diff --git a/src/emulator/sms/mod.rs b/src/emulator/sms/mod.rs new file mode 100644 index 0000000..99206ee --- /dev/null +++ b/src/emulator/sms/mod.rs @@ -0,0 +1,115 @@ +//! Support for attaching to SEGA Master System / SEGA GameGear emulators. + +use crate::{Address, Error, Process}; +use bytemuck::CheckedBitPattern; + +mod blastem; +mod fusion; +mod mednafen; +mod retroarch; + +/// A SEGA Master System / GameGear emulator that the auto splitter is attached to. +pub struct Emulator { + /// The attached emulator process + process: Process, + /// An enum stating which emulator is currently attached + state: State, + /// The memory address of the emulated RAM + ram_base: Option
, +} + +impl Emulator { + /// Attaches to the emulator process + /// + /// Returns `Option` if successful, `None` otherwise. + /// + /// Supported emulators are: + /// - Retroarch, with one of the following cores: `genesis_plus_gx_libretro.dll`, + /// `genesis_plus_gx_wide_libretro.dll`, `picodrive_libretro.dll`, `smsplus_libretro.dll`, `gearsystem_libretro.dll` + /// - Fusion + /// - BlastEm + pub fn attach() -> Option { + let (&state, process) = PROCESS_NAMES + .iter() + .find_map(|(name, state)| Some((state, Process::attach(name)?)))?; + + Some(Self { + process, + state, + ram_base: None, + }) + } + + /// Checks whether the emulator is still open. If it is not open anymore, + /// you should drop the emulator. + pub fn is_open(&self) -> bool { + self.process.is_open() + } + + /// Calls the internal routines needed in order to find (and update, if + /// needed) the address of the emulated RAM. + /// + /// Returns true if successful, false otherwise. + pub fn update(&mut self) -> bool { + if self.ram_base.is_none() { + self.ram_base = match match &mut self.state { + State::Retroarch(x) => x.find_ram(&self.process), + State::Fusion(x) => x.find_ram(&self.process), + State::BlastEm(x) => x.find_ram(&self.process), + State::Mednafen(x) => x.find_ram(&self.process), + } { + None => return false, + something => something, + }; + } + + let success = match &self.state { + State::Retroarch(x) => x.keep_alive(&self.process), + State::Fusion(x) => x.keep_alive(&self.process, &mut self.ram_base), + State::BlastEm(x) => x.keep_alive(), + State::Mednafen(x) => x.keep_alive(), + }; + + if success { + true + } else { + self.ram_base = None; + false + } + } + + /// Reads any value from the emulated RAM. + /// + /// The offset provided is meant to be the same used on the original hardware. + /// + /// The SEGA Master System has 8KB of RAM, mapped from address + /// `0xC000` to `0xDFFF`. + /// + /// Providing any offset outside this range will return `Err()`. + pub fn read(&self, offset: u32) -> Result { + if (offset > 0x1FFF && offset < 0xC000) || offset > 0xDFFF { + return Err(Error {}); + } + + let wram = self.ram_base.ok_or(Error {})?; + let end_offset = offset.checked_sub(0xC000).unwrap_or(offset); + + self.process.read(wram + end_offset) + } +} + +#[doc(hidden)] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum State { + Retroarch(retroarch::State), + Fusion(fusion::State), + BlastEm(blastem::State), + Mednafen(mednafen::State), +} + +static PROCESS_NAMES: &[(&str, State)] = &[ + ("retroarch.exe", State::Retroarch(retroarch::State::new())), + ("Fusion.exe", State::Fusion(fusion::State::new())), + ("blastem.exe", State::BlastEm(blastem::State)), + ("mednafen.exe", State::Mednafen(mednafen::State)), +]; diff --git a/src/emulator/sms/retroarch.rs b/src/emulator/sms/retroarch.rs new file mode 100644 index 0000000..1ad53d7 --- /dev/null +++ b/src/emulator/sms/retroarch.rs @@ -0,0 +1,133 @@ +use crate::{file_format::pe, signature::Signature, Address, Address32, Address64, Process}; + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct State { + core_base: Address, +} + +impl State { + pub fn find_ram(&mut self, game: &Process) -> Option
{ + const SUPPORTED_CORES: &[&str] = &[ + "genesis_plus_gx_libretro.dll", + "genesis_plus_gx_wide_libretro.dll", + "picodrive_libretro.dll", + "smsplus_libretro.dll", + "gearsystem_libretro.dll", + ]; + + let main_module_address = super::PROCESS_NAMES + .iter() + .filter(|(_, state)| matches!(state, super::State::Retroarch(_))) + .find_map(|(name, _)| game.get_module_address(name).ok())?; + + let is_64_bit = + pe::MachineType::read(game, main_module_address) == Some(pe::MachineType::X86_64); + + let (core_name, core_address) = SUPPORTED_CORES + .iter() + .find_map(|&m| Some((m, game.get_module_address(m).ok()?)))?; + + self.core_base = core_address; + + match core_name { + "genesis_plus_gx_libretro.dll" | "genesis_plus_gx_wide_libretro.dll" => { + self.genesis_plus(game, is_64_bit, core_name) + } + "picodrive_libretro.dll" => self.picodrive(game, is_64_bit, core_name), + "smsplus_libretro.dll" => self.sms_plus(game, is_64_bit, core_name), + "gearsystem_libretro.dll" => self.gearsystem(game, is_64_bit, core_name), + _ => None, + } + } + + fn picodrive(&self, game: &Process, is_64_bit: bool, core_name: &str) -> Option
{ + let module_size = game.get_module_size(core_name).ok()?; + + Some( + if is_64_bit { + const SIG: Signature<9> = Signature::new("48 8D 0D ?? ?? ?? ?? 41 B8"); + let ptr = SIG.scan_process_range(game, (self.core_base, module_size))? + 3; + ptr + 0x4 + game.read::(ptr).ok()? + } else { + const SIG: Signature<8> = Signature::new("B9 ?? ?? ?? ?? C1 EF 10"); + let ptr = SIG.scan_process_range(game, (self.core_base, module_size))? + 1; + game.read::(ptr).ok()?.into() + } + 0x20000, + ) + } + + fn genesis_plus(&self, game: &Process, is_64_bit: bool, core_name: &str) -> Option
{ + let module_size = game.get_module_size(core_name).ok()?; + + Some(if is_64_bit { + const SIG: Signature<10> = Signature::new("48 8D 0D ?? ?? ?? ?? 4C 8B 2D"); + let ptr = SIG.scan_process_range(game, (self.core_base, module_size))? + 3; + ptr + 0x4 + game.read::(ptr).ok()? + } else { + const SIG: Signature<7> = Signature::new("A3 ?? ?? ?? ?? 29 F9"); + let ptr = SIG.scan_process_range(game, (self.core_base, module_size))? + 1; + game.read::(ptr).ok()?.into() + }) + } + + fn sms_plus(&self, game: &Process, is_64_bit: bool, core_name: &str) -> Option
{ + let module_size = game.get_module_size(core_name).ok()?; + + Some(if is_64_bit { + const SIG: Signature<5> = Signature::new("31 F6 48 C7 05"); + let ptr = SIG.scan_process_range(game, (self.core_base, module_size))? + 5; + ptr + 0x8 + game.read::(ptr).ok()? + } else { + const SIG: Signature<4> = Signature::new("83 FA 02 B8"); + let ptr = SIG.scan_process_range(game, (self.core_base, module_size))? + 4; + game.read::(ptr).ok()?.into() + }) + } + + fn gearsystem(&self, game: &Process, is_64_bit: bool, core_name: &str) -> Option
{ + let module_size = game.get_module_size(core_name).ok()?; + + Some(if is_64_bit { + const SIG: Signature<13> = Signature::new("83 ?? 02 75 ?? 48 8B 0D ?? ?? ?? ?? E8"); + let ptr = SIG.scan_process_range(game, (self.core_base, module_size))? + 8; + let offset = game + .read::(ptr + 13 + 0x4 + game.read::(ptr + 13).ok()? + 3) + .ok()?; + let addr = game + .read_pointer_path64::( + ptr + 0x4 + game.read::(ptr).ok()?, + &[0x0, 0x0, offset as _], + ) + .ok()?; + if addr.is_null() { + return None; + } else { + addr.add(0xC000).into() + } + } else { + const SIG: Signature<12> = Signature::new("83 ?? 02 75 ?? 8B ?? ?? ?? ?? ?? E8"); + let ptr = SIG.scan_process_range(game, (self.core_base, module_size))? + 7; + let offset = game + .read::(ptr + 12 + 0x4 + game.read::(ptr + 12).ok()? + 2) + .ok()?; + let addr = game + .read_pointer_path32::(ptr, &[0x0, 0x0, 0x0, offset as _]) + .ok()?; + if addr.is_null() { + return None; + } else { + addr.add(0xC000).into() + } + }) + } + + pub fn keep_alive(&self, game: &Process) -> bool { + game.read::(self.core_base).is_ok() + } + + pub const fn new() -> Self { + Self { + core_base: Address::NULL, + } + } +}