-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Added support for PS2 emulators (#54)
This adds support for coding auto splitters for PS2 emulators. Code and the general inner workings of the ps2::Emulator struct uses the same implementation seen previously in #26 and #33 So, in order to use it: 1. Enable the `ps2` feature in your `Cargo.toml` 2. Attach to the supported PS2 emulators by running `ps2::Emulator::attach()` 3. Run the internal `update() -> bool` to locate and update the address of the emulated game's memory 4. Read from the game's memory via its `read` function. Supported Windows emulators: - PCSX2 - Retroarch (using the `pcsx2_libretro.dll` core)
- Loading branch information
Showing
5 changed files
with
251 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
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,3 +8,5 @@ pub mod gcn; | |
pub mod genesis; | ||
#[cfg(feature = "ps1")] | ||
pub mod ps1; | ||
#[cfg(feature = "ps2")] | ||
pub mod ps2; |
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,134 @@ | ||
//! Support for attaching to Playstation 2 emulators. | ||
|
||
use crate::{Address, Error, Process}; | ||
use bytemuck::CheckedBitPattern; | ||
|
||
mod pcsx2; | ||
mod retroarch; | ||
|
||
/// A Playstation 2 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: | ||
/// - PCSX2 | ||
/// - Retroarch (64-bit version, using the `pcsx2_libretro.dll` core) | ||
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() { | ||
let ram_base = match &mut self.state { | ||
State::Pcsx2(x) => x.find_ram(&self.process), | ||
State::Retroarch(x) => x.find_ram(&self.process), | ||
}; | ||
if ram_base.is_none() { | ||
return false; | ||
} | ||
self.ram_base = ram_base; | ||
} | ||
|
||
let success = match &self.state { | ||
State::Pcsx2(x) => x.keep_alive(&self.process, &mut self.ram_base), | ||
State::Retroarch(x) => x.keep_alive(&self.process), | ||
}; | ||
|
||
if !success { | ||
self.ram_base = None; | ||
} | ||
|
||
success | ||
} | ||
|
||
/// Reads any value from the emulated RAM. | ||
/// | ||
/// In PS2, memory addresses are mapped at fixed locations starting | ||
/// from `0x00100000` (addresses below this threashold are | ||
/// reserved for the kernel). | ||
/// | ||
/// Valid addresses for the PS2's memory range from `0x00100000` to `0x01FFFFFF` | ||
/// | ||
/// Providing any offset outside the range of the PS2's RAM will return | ||
/// `Err()`. | ||
pub fn read<T: CheckedBitPattern>(&self, address: u32) -> Result<T, Error> { | ||
if !(0x00100000..=0x01FFFFFF).contains(&address) { | ||
return Err(Error {}); | ||
} | ||
|
||
let Some(ram_base) = self.ram_base else { | ||
return Err(Error {}); | ||
}; | ||
|
||
self.process.read(ram_base + address) | ||
} | ||
|
||
/// Follows a path of pointers from the base address given and reads a value of the | ||
/// type specified at the end of the pointer path. | ||
/// | ||
/// In PS2, memory addresses are mapped at fixed locations starting | ||
/// from `0x00100000` (addresses below this threashold are | ||
/// reserved for the kernel). | ||
/// | ||
/// Valid addresses for the PS2's memory range from `0x00100000` to `0x01FFFFFF` | ||
/// | ||
/// Providing any offset outside the range of the PS2's RAM will return | ||
/// `Err()`. | ||
pub fn read_pointer_path<T: CheckedBitPattern>( | ||
&self, | ||
base_address: u32, | ||
path: &[u32], | ||
) -> Result<T, Error> { | ||
let mut address = base_address; | ||
let (&last, path) = path.split_last().ok_or(Error {})?; | ||
for &offset in path { | ||
address = self.read(address + offset)?; | ||
} | ||
self.read(address + last) | ||
} | ||
} | ||
|
||
#[doc(hidden)] | ||
#[derive(Copy, Clone, Debug, PartialEq, Eq)] | ||
enum State { | ||
Pcsx2(pcsx2::State), | ||
Retroarch(retroarch::State), | ||
} | ||
|
||
const PROCESS_NAMES: [(&str, State); 6] = [ | ||
("pcsx2x64.exe", State::Pcsx2(pcsx2::State::new())), | ||
("pcsx2-qt.exe", State::Pcsx2(pcsx2::State::new())), | ||
("pcsx2x64-avx2.exe", State::Pcsx2(pcsx2::State::new())), | ||
("pcsx2-avx2.exe", State::Pcsx2(pcsx2::State::new())), | ||
("pcsx2.exe", State::Pcsx2(pcsx2::State::new())), | ||
("retroarch.exe", State::Retroarch(retroarch::State::new())), | ||
]; |
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,58 @@ | ||
use crate::{file_format::pe, signature::Signature, Address, Address32, Address64, Error, Process}; | ||
|
||
#[derive(Copy, Clone, Debug, PartialEq, Eq)] | ||
pub struct State { | ||
is_64_bit: bool, | ||
addr_base: Address, | ||
} | ||
|
||
impl State { | ||
pub fn find_ram(&mut self, game: &Process) -> Option<Address> { | ||
let main_module_range = super::PROCESS_NAMES | ||
.iter() | ||
.filter(|(_, state)| matches!(state, super::State::Pcsx2(_))) | ||
.find_map(|(name, _)| game.get_module_range(name).ok())?; | ||
|
||
self.is_64_bit = | ||
pe::MachineType::read(game, main_module_range.0) == Some(pe::MachineType::X86_64); | ||
|
||
self.addr_base = if self.is_64_bit { | ||
const SIG: Signature<12> = Signature::new("48 8B ?? ?? ?? ?? ?? 25 F0 3F 00 00"); | ||
let ptr = SIG.scan_process_range(game, main_module_range)? + 3; | ||
ptr + 0x4 + game.read::<i32>(ptr).ok()? | ||
} else { | ||
const SIG: Signature<11> = Signature::new("8B ?? ?? ?? ?? ?? 25 F0 3F 00 00"); | ||
const SIG_ALT: Signature<12> = Signature::new("8B ?? ?? ?? ?? ?? 81 ?? F0 3F 00 00"); | ||
let ptr = if let Some(addr) = SIG.scan_process_range(game, main_module_range) { | ||
addr + 2 | ||
} else { | ||
SIG_ALT.scan_process_range(game, main_module_range)? + 2 | ||
}; | ||
self.read_pointer(game, ptr).ok()? | ||
}; | ||
|
||
self.read_pointer(game, self.addr_base).ok() | ||
} | ||
|
||
pub fn keep_alive(&self, game: &Process, ram_base: &mut Option<Address>) -> bool { | ||
*ram_base = Some(match self.read_pointer(game, self.addr_base) { | ||
Ok(x) => x, | ||
Err(_) => return false, | ||
}); | ||
true | ||
} | ||
|
||
fn read_pointer(&self, game: &Process, address: Address) -> Result<Address, Error> { | ||
Ok(match self.is_64_bit { | ||
true => game.read::<Address64>(address)?.into(), | ||
false => game.read::<Address32>(address)?.into(), | ||
}) | ||
} | ||
|
||
pub const fn new() -> Self { | ||
Self { | ||
is_64_bit: true, | ||
addr_base: 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,56 @@ | ||
use crate::{file_format::pe, signature::Signature, Address, 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; 1] = ["pcsx2_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); | ||
|
||
if !is_64_bit { | ||
// The LRPS2 core, the only one available for retroarch at | ||
// the time of writing (Sep 14th, 2023), only supports 64-bit | ||
return None; | ||
} | ||
|
||
let (core_name, core_address) = SUPPORTED_CORES | ||
.iter() | ||
.find_map(|&m| Some((m, game.get_module_address(m).ok()?)))?; | ||
|
||
self.core_base = core_address; | ||
|
||
let base_addr = { | ||
const SIG: Signature<13> = Signature::new("48 8B ?? ?? ?? ?? ?? 81 ?? F0 3F 00 00"); | ||
let ptr = SIG | ||
.scan_process_range(game, (core_address, game.get_module_size(core_name).ok()?))? | ||
+ 3; | ||
ptr + 0x4 + game.read::<i32>(ptr).ok()? | ||
}; | ||
|
||
match game.read::<Address64>(base_addr) { | ||
Ok(Address64::NULL) => None, | ||
Ok(x) => Some(x.into()), | ||
_ => None, | ||
} | ||
} | ||
|
||
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, | ||
} | ||
} | ||
} |