From b7276cbb743650ceec510faef66fbbafbed53c34 Mon Sep 17 00:00:00 2001 From: Jujstme Date: Sun, 24 Sep 2023 16:34:47 +0200 Subject: [PATCH] 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 https://github.com/LiveSplit/asr/pull/26 and https://github.com/LiveSplit/asr/pull/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) --- Cargo.toml | 1 + src/emulator/mod.rs | 2 + src/emulator/ps2/mod.rs | 134 ++++++++++++++++++++++++++++++++++ src/emulator/ps2/pcsx2.rs | 58 +++++++++++++++ src/emulator/ps2/retroarch.rs | 56 ++++++++++++++ 5 files changed, 251 insertions(+) create mode 100644 src/emulator/ps2/mod.rs create mode 100644 src/emulator/ps2/pcsx2.rs create mode 100644 src/emulator/ps2/retroarch.rs diff --git a/Cargo.toml b/Cargo.toml index 542dde1..36c5270 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,3 +37,4 @@ gba = [] gcn = ["flags"] genesis = ["flags", "signature"] ps1 = ["flags", "signature"] +ps2 = ["flags", "signature"] diff --git a/src/emulator/mod.rs b/src/emulator/mod.rs index befe2f7..4ae865f 100644 --- a/src/emulator/mod.rs +++ b/src/emulator/mod.rs @@ -8,3 +8,5 @@ pub mod gcn; pub mod genesis; #[cfg(feature = "ps1")] pub mod ps1; +#[cfg(feature = "ps2")] +pub mod ps2; diff --git a/src/emulator/ps2/mod.rs b/src/emulator/ps2/mod.rs new file mode 100644 index 0000000..77c4b6d --- /dev/null +++ b/src/emulator/ps2/mod.rs @@ -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
, +} + +impl Emulator { + /// Attaches to the emulator process + /// + /// Returns `Option` if successful, `None` otherwise. + /// + /// Supported emulators are: + /// - PCSX2 + /// - Retroarch (64-bit version, using the `pcsx2_libretro.dll` core) + 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() { + 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(&self, address: u32) -> Result { + 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( + &self, + base_address: u32, + path: &[u32], + ) -> Result { + 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())), +]; diff --git a/src/emulator/ps2/pcsx2.rs b/src/emulator/ps2/pcsx2.rs new file mode 100644 index 0000000..96b36ab --- /dev/null +++ b/src/emulator/ps2/pcsx2.rs @@ -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
{ + 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::(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
) -> 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 { + Ok(match self.is_64_bit { + true => game.read::(address)?.into(), + false => game.read::(address)?.into(), + }) + } + + pub const fn new() -> Self { + Self { + is_64_bit: true, + addr_base: Address::NULL, + } + } +} diff --git a/src/emulator/ps2/retroarch.rs b/src/emulator/ps2/retroarch.rs new file mode 100644 index 0000000..5b38c76 --- /dev/null +++ b/src/emulator/ps2/retroarch.rs @@ -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
{ + 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::(ptr).ok()? + }; + + match game.read::(base_addr) { + Ok(Address64::NULL) => None, + Ok(x) => Some(x.into()), + _ => None, + } + } + + 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, + } + } +}