-
Notifications
You must be signed in to change notification settings - Fork 11
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #58 from Jujstme/sms
Added support for Sega Master System / Game Gear (SMS) emulators
- Loading branch information
Showing
7 changed files
with
351 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<Address> { | ||
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::<Address32>(scanned_address).ok()?.into(); | ||
|
||
if wram.is_null() { | ||
None | ||
} else { | ||
Some(wram) | ||
} | ||
} | ||
|
||
pub const fn keep_alive(&self) -> bool { | ||
true | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<Address> { | ||
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::<Address32>(ptr).ok()?.into(); | ||
|
||
Some(game.read::<Address32>(self.addr).ok()?.add(0xC000).into()) | ||
} | ||
|
||
pub fn keep_alive(&self, game: &Process, wram_base: &mut Option<Address>) -> bool { | ||
*wram_base = Some(match game.read::<Address32>(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, | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<Address> { | ||
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::<Address32>(ptr).ok()?.into()) | ||
} | ||
|
||
pub const fn keep_alive(&self) -> bool { | ||
true | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<Address>, | ||
} | ||
|
||
impl Emulator { | ||
/// Attaches to the emulator process | ||
/// | ||
/// Returns `Option<T>` 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<Self> { | ||
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<T: CheckedBitPattern>(&self, offset: u32) -> Result<T, Error> { | ||
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)), | ||
]; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<Address> { | ||
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<Address> { | ||
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::<i32>(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::<Address32>(ptr).ok()?.into() | ||
} + 0x20000, | ||
) | ||
} | ||
|
||
fn genesis_plus(&self, game: &Process, is_64_bit: bool, core_name: &str) -> Option<Address> { | ||
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::<i32>(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::<Address32>(ptr).ok()?.into() | ||
}) | ||
} | ||
|
||
fn sms_plus(&self, game: &Process, is_64_bit: bool, core_name: &str) -> Option<Address> { | ||
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::<i32>(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::<Address32>(ptr).ok()?.into() | ||
}) | ||
} | ||
|
||
fn gearsystem(&self, game: &Process, is_64_bit: bool, core_name: &str) -> Option<Address> { | ||
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::<u8>(ptr + 13 + 0x4 + game.read::<i32>(ptr + 13).ok()? + 3) | ||
.ok()?; | ||
let addr = game | ||
.read_pointer_path64::<Address64>( | ||
ptr + 0x4 + game.read::<i32>(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::<u8>(ptr + 12 + 0x4 + game.read::<i32>(ptr + 12).ok()? + 2) | ||
.ok()?; | ||
let addr = game | ||
.read_pointer_path32::<Address32>(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::<u8>(self.core_base).is_ok() | ||
} | ||
|
||
pub const fn new() -> Self { | ||
Self { | ||
core_base: Address::NULL, | ||
} | ||
} | ||
} |