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,
+ }
+ }
+}