Skip to content

Commit

Permalink
Merge pull request #58 from Jujstme/sms
Browse files Browse the repository at this point in the history
Added support for Sega Master System / Game Gear (SMS) emulators
  • Loading branch information
CryZe committed Dec 1, 2023
2 parents 0ad1d1f + 35b295d commit b23ec61
Show file tree
Hide file tree
Showing 7 changed files with 351 additions and 0 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,5 @@ gcn = ["flags"]
genesis = ["flags", "signature"]
ps1 = ["flags", "signature"]
ps2 = ["flags", "signature"]
sms = ["flags", "signature"]
wii = ["flags"]
2 changes: 2 additions & 0 deletions src/emulator/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
33 changes: 33 additions & 0 deletions src/emulator/sms/blastem.rs
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
}
}
37 changes: 37 additions & 0 deletions src/emulator/sms/fusion.rs
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,
}
}
}
30 changes: 30 additions & 0 deletions src/emulator/sms/mednafen.rs
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
}
}
115 changes: 115 additions & 0 deletions src/emulator/sms/mod.rs
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)),
];
133 changes: 133 additions & 0 deletions src/emulator/sms/retroarch.rs
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,
}
}
}

0 comments on commit b23ec61

Please sign in to comment.