use log::{debug, error, trace};
use process_memory::TryIntoProcessHandle;
use sysinfo::{ProcessExt, System, SystemExt};
use tap::TapFallible;
use thiserror::Error;
use crate::game_interface::{GameInterface, InterfaceError};
use self::dolphin_var::DolphinBackend;
use super::{InterfaceProvider, InterfaceResult};
mod data_member;
pub mod dolphin_var;
const REGION_SIZE: usize = 0x200_0000;
#[cfg(target_os = "linux")]
const PROCESS_NAME: &str = "dolphin-emu";
#[cfg(target_os = "windows")]
const PROCESS_NAME: &str = "Dolphin";
#[derive(Debug, Error)]
pub enum Error {
#[error("The emulated memory region could not be found. Make sure Dolphin is running and the game is open.")]
RegionNotFound,
#[error("Dolphin memory could not be accessed.")]
IO,
}
impl From<std::io::Error> for Error {
fn from(_: std::io::Error) -> Self {
Self::IO
}
}
pub struct DolphinInterface {
system: System,
state: DolphinState,
}
#[allow(clippy::large_enum_variant)]
enum DolphinState {
Unhooked,
Hooked(GameInterface<DolphinBackend>),
}
impl Default for DolphinInterface {
fn default() -> Self {
Self {
system: System::default(),
state: DolphinState::Unhooked,
}
}
}
impl InterfaceProvider for DolphinInterface {
type Backend = DolphinBackend;
fn do_with_interface<T>(
&mut self,
fun: impl FnOnce(&mut GameInterface<Self::Backend>) -> InterfaceResult<T>,
) -> InterfaceResult<T> {
let interface = self.get_interface_or_hook();
fun(interface?).tap_err(|e| {
if let InterfaceError::Unhooked = e {
trace!("Unhooked from Dolphin");
self.state = DolphinState::Unhooked;
}
})
}
fn is_available(&mut self) -> bool {
self.get_interface_or_hook().is_ok()
}
}
impl DolphinInterface {
fn get_interface_or_hook(&mut self) -> InterfaceResult<&mut GameInterface<DolphinBackend>> {
let interface = match self.state {
DolphinState::Unhooked => {
let interface = self.hook()?;
self.state = DolphinState::Hooked(interface);
match self.state {
DolphinState::Unhooked => unreachable!(),
DolphinState::Hooked(ref mut interface) => interface,
}
}
DolphinState::Hooked(ref mut interface) => interface,
};
Ok(interface)
}
fn hook(&mut self) -> InterfaceResult<GameInterface<DolphinBackend>> {
self.system.refresh_processes();
let procs = self.system.processes_by_name(PROCESS_NAME);
if let Some((pid, addr)) = procs
.into_iter()
.map(|p| {
let pid = p.pid();
trace!("{} found with pid {pid}", p.name());
(pid, get_emulated_base_address(pid))
})
.find_map(|(pid, addr)| addr.map(|addr| (pid, addr)))
{
debug!("Found emulated memory region at {addr:#X}");
let base_address = addr;
#[cfg(target_os = "windows")]
let pid = <sysinfo::Pid as Into<usize>>::into(pid) as u32;
#[allow(clippy::useless_conversion)]
let pid: process_memory::Pid = pid.into();
let handle = pid.try_into_process_handle()?;
return Ok(GameInterface::<DolphinBackend>::new(base_address, handle));
}
Err(InterfaceError::HookingFailed)
}
}
#[cfg(target_os = "linux")]
fn get_emulated_base_address(pid: sysinfo::Pid) -> Option<usize> {
use proc_maps::get_process_maps;
let maps = match get_process_maps(pid.into()) {
Err(e) => {
error!("Could not get dolphin process maps\n{e:?}");
return None;
}
Ok(maps) => maps,
};
let map = maps.iter().rev().find(|m| {
m.size() == REGION_SIZE
&& m.filename()
.map(|f| f.to_string_lossy().contains("dolphin-emu"))
.unwrap_or(false)
});
map.map(|m| m.start())
}
#[cfg(target_os = "windows")]
fn get_emulated_base_address(pid: sysinfo::Pid) -> Option<usize> {
use winapi::um::handleapi::CloseHandle;
use winapi::um::memoryapi::VirtualQueryEx;
use winapi::um::processthreadsapi::OpenProcess;
use winapi::um::psapi::{QueryWorkingSetEx, PSAPI_WORKING_SET_EX_INFORMATION};
use winapi::um::winnt::{
MEMORY_BASIC_INFORMATION, MEM_MAPPED, PROCESS_QUERY_INFORMATION, PROCESS_VM_OPERATION,
PROCESS_VM_READ, PROCESS_VM_WRITE,
};
unsafe {
let handle = OpenProcess(
PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION | PROCESS_VM_READ | PROCESS_VM_WRITE,
0,
<sysinfo::Pid as Into<usize>>::into(pid) as u32,
);
if handle.is_null() {
error!("Failed to open process handle for dolphin (pid {pid})");
return None;
}
let mut mem_info = MEMORY_BASIC_INFORMATION::default();
let mut mem = std::ptr::null();
while VirtualQueryEx(
handle,
mem,
&mut mem_info,
std::mem::size_of::<MEMORY_BASIC_INFORMATION>(),
) == std::mem::size_of::<MEMORY_BASIC_INFORMATION>()
{
if mem_info.RegionSize == REGION_SIZE && mem_info.Type == MEM_MAPPED {
let mut ws_info = PSAPI_WORKING_SET_EX_INFORMATION {
VirtualAddress: mem_info.BaseAddress,
..Default::default()
};
if QueryWorkingSetEx(
handle,
std::ptr::addr_of_mut!(ws_info).cast::<std::ffi::c_void>(),
std::mem::size_of::<PSAPI_WORKING_SET_EX_INFORMATION>()
.try_into()
.unwrap(),
) != 0
&& ws_info.VirtualAttributes.Valid() != 0
{
return Some(mem_info.BaseAddress as usize);
}
}
mem = mem.add(mem_info.RegionSize);
}
CloseHandle(handle);
}
None
}