-
Notifications
You must be signed in to change notification settings - Fork 10
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 Wii emulators (#59)
- Loading branch information
Showing
5 changed files
with
307 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 |
---|---|---|
|
@@ -10,3 +10,5 @@ pub mod genesis; | |
pub mod ps1; | ||
#[cfg(feature = "ps2")] | ||
pub mod ps2; | ||
#[cfg(feature = "wii")] | ||
pub mod wii; |
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,46 @@ | ||
use crate::{Address, Endian, FromEndian, MemoryRangeFlags, Process}; | ||
|
||
#[derive(Copy, Clone, Debug, PartialEq, Eq)] | ||
pub struct State; | ||
|
||
impl State { | ||
pub fn find_ram(&self, game: &Process, endian: &mut Endian) -> Option<[Address; 2]> { | ||
let mem_1 = game | ||
.memory_ranges() | ||
.find(|range| { | ||
range | ||
.flags() | ||
.is_ok_and(|r| r.contains(MemoryRangeFlags::WRITE | MemoryRangeFlags::READ)) | ||
&& range.size().is_ok_and(|size| size == 0x2000000) | ||
&& range.address().is_ok_and(|addr| { | ||
game.read::<[u32; 2]>(addr + 0x3118) | ||
.is_ok_and(|val| val.from_endian(Endian::Big) == [0x4000000; 2]) | ||
}) | ||
})? | ||
.address() | ||
.ok()?; | ||
|
||
let mem_2 = game | ||
.memory_ranges() | ||
.find(|range| { | ||
range | ||
.flags() | ||
.is_ok_and(|r| r.contains(MemoryRangeFlags::WRITE | MemoryRangeFlags::READ)) | ||
&& range.size().is_ok_and(|size| size == 0x4000000) | ||
&& range | ||
.address() | ||
.is_ok_and(|addr| addr > mem_1 && addr < mem_1 + 0x10000000) | ||
})? | ||
.address() | ||
.ok()?; | ||
|
||
*endian = Endian::Big; | ||
Some([mem_1, mem_2]) | ||
} | ||
|
||
pub fn keep_alive(&self, game: &Process, ram_base: &Option<[Address; 2]>) -> bool { | ||
ram_base.is_some_and(|[mem1, mem2]| { | ||
game.read::<u8>(mem1).is_ok() && game.read::<u8>(mem2).is_ok() | ||
}) | ||
} | ||
} |
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,213 @@ | ||
//! Support for attaching to Nintendo Wii emulators. | ||
use crate::{Address, Endian, Error, FromEndian, Process}; | ||
use bytemuck::CheckedBitPattern; | ||
|
||
mod dolphin; | ||
mod retroarch; | ||
|
||
/// A Nintendo Wii emulator that the auto splitter is attached to, | ||
/// for supporting Wii and WiiWare games. | ||
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; 2]>, // [MEM1, MEM2] | ||
/// The endianness used by the emulator process | ||
endian: Endian, | ||
} | ||
|
||
impl Emulator { | ||
/// Attaches to the emulator process | ||
/// | ||
/// Returns `Option<Emulator>` if successful, `None` otherwise. | ||
/// | ||
/// Supported emulators are: | ||
/// - Dolphin | ||
/// - Retroarch (using the `dolphin_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, // [MEM1, MEM2] | ||
endian: Endian::Big, // Endianness is usually Big in Wii emulators | ||
}) | ||
} | ||
|
||
/// 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::Dolphin(x) => x.find_ram(&self.process, &mut self.endian), | ||
State::Retroarch(x) => x.find_ram(&self.process, &mut self.endian), | ||
} { | ||
None => return false, | ||
something => something, | ||
}; | ||
} | ||
|
||
let success = match &self.state { | ||
State::Dolphin(x) => x.keep_alive(&self.process, &self.ram_base), | ||
State::Retroarch(x) => x.keep_alive(&self.process, &self.ram_base), | ||
}; | ||
|
||
if success { | ||
true | ||
} else { | ||
self.ram_base = None; | ||
false | ||
} | ||
} | ||
|
||
/// Reads raw data from the emulated RAM ignoring all endianness settings. | ||
/// The same call, performed on two different emulators, might return different | ||
/// results due to the endianness used by the emulator. | ||
/// | ||
/// The address provided is meant to be the mapped address used on the original, big-endian system. | ||
/// The call will automatically convert the address provided to its corresponding offset from | ||
/// `MEM1` or `MEM2` and read the value. | ||
/// | ||
/// The provided memory address has to match a mapped memory address on the original Wii: | ||
/// - Valid addresses for `MEM1` range from `0x80000000` to `0x817FFFFF` | ||
/// - Valid addresses for `MEM2` range from `0x90000000` to `0x93FFFFFF` | ||
/// | ||
/// Any other invalid value will make this method immediately return `Err()`. | ||
/// | ||
/// This call is meant to be used by experienced users. | ||
pub fn read_ignoring_endianness<T: CheckedBitPattern>(&self, address: u32) -> Result<T, Error> { | ||
if address >= 0x80000000 && address <= 0x817FFFFF { | ||
self.read_ignoring_endianness_from_mem_1(address) | ||
} else if address >= 0x90000000 && address <= 0x93FFFFFF { | ||
self.read_ignoring_endianness_from_mem_2(address) | ||
} else { | ||
Err(Error {}) | ||
} | ||
} | ||
|
||
/// Reads any value from the emulated RAM. | ||
/// | ||
/// The offset provided is meant to be the mapped address used on the original, big-endian system. | ||
/// The call will automatically convert the address provided to its corresponding offset from | ||
/// `MEM1` or `MEM2` and read the value, providing conversion from Big Endian to Little Endian. | ||
/// | ||
/// The provided memory address has to match a mapped memory address on the original Wii: | ||
/// - Valid addresses for `MEM1` range from `0x80000000` to `0x817FFFFF` | ||
/// - Valid addresses for `MEM2` range from `0x90000000` to `0x93FFFFFF` | ||
/// | ||
/// Any other invalid value will make this method immediately return `Err()`. | ||
pub fn read<T: CheckedBitPattern + FromEndian>(&self, address: u32) -> Result<T, Error> { | ||
Ok(self | ||
.read_ignoring_endianness::<T>(address)? | ||
.from_endian(self.endian)) | ||
} | ||
|
||
/// Follows a path of pointers from the address given and reads a value of the type specified from | ||
/// the process at the end of the pointer path. | ||
/// | ||
/// The end value is automatically converted to little endian if needed. | ||
pub fn read_pointer_path<T: CheckedBitPattern + FromEndian>( | ||
&self, | ||
base_address: u32, | ||
path: &[u32], | ||
) -> Result<T, Error> { | ||
self.read(self.deref_offsets(base_address, path)?) | ||
} | ||
|
||
/// Follows a path of pointers from the address given and reads a value of the type specified from | ||
/// the process at the end of the pointer path. | ||
pub fn read_pointer_path_ignoring_endianness<T: CheckedBitPattern>( | ||
&self, | ||
base_address: u32, | ||
path: &[u32], | ||
) -> Result<T, Error> { | ||
self.read_ignoring_endianness(self.deref_offsets(base_address, path)?) | ||
} | ||
|
||
fn deref_offsets(&self, base_address: u32, path: &[u32]) -> Result<u32, Error> { | ||
let mut address = base_address; | ||
let (&last, path) = path.split_last().ok_or(Error {})?; | ||
for &offset in path { | ||
address = self.read::<u32>(address + offset)?; | ||
} | ||
Ok(address + last) | ||
} | ||
|
||
/// Reads raw data from the emulated RAM ignoring all endianness settings. | ||
/// The same call, performed on two different emulators, might return different | ||
/// results due to the endianness used by the emulator. | ||
/// | ||
/// The address provided is meant to be the mapped address used on the original, big-endian system. | ||
/// The call will automatically convert the address provided to its corresponding offset from | ||
/// `MEM1` or and read the value. | ||
/// | ||
/// The provided memory address has to match a mapped memory address on the original Wii. | ||
/// Valid addresses for `MEM1` range from `0x80000000` to `0x817FFFFF` | ||
/// | ||
/// Any other invalid value will make this method immediately return `Err()`. | ||
pub fn read_ignoring_endianness_from_mem_1<T: CheckedBitPattern>( | ||
&self, | ||
address: u32, | ||
) -> Result<T, Error> { | ||
if address < 0x80000000 || address > 0x817FFFFF { | ||
return Err(Error {}); | ||
} | ||
let Some([mem1, _]) = self.ram_base else { | ||
return Err(Error {}); | ||
}; | ||
let end_offset = address.checked_sub(0x80000000).unwrap_or(address); | ||
self.process.read(mem1 + end_offset) | ||
} | ||
|
||
/// Reads raw data from the emulated RAM ignoring all endianness settings. | ||
/// The same call, performed on two different emulators, might return different | ||
/// results due to the endianness used by the emulator. | ||
/// | ||
/// The address provided is meant to be the mapped address used on the original, big-endian system. | ||
/// The call will automatically convert the address provided to its corresponding offset from | ||
/// `MEM2` or and read the value. | ||
/// | ||
/// The provided memory address has to match a mapped memory address on the original Wii. | ||
/// Valid addresses for `MEM2` range from `0x90000000` to `0x93FFFFFF` | ||
/// | ||
/// Any other invalid value will make this method immediately return `Err()`. | ||
pub fn read_ignoring_endianness_from_mem_2<T: CheckedBitPattern>( | ||
&self, | ||
address: u32, | ||
) -> Result<T, Error> { | ||
if address < 0x90000000 || address > 0x93FFFFFF { | ||
return Err(Error {}); | ||
} | ||
let Some([_, mem2]) = self.ram_base else { | ||
return Err(Error {}); | ||
}; | ||
let end_offset = address.checked_sub(0x90000000).unwrap_or(address); | ||
self.process.read(mem2 + end_offset) | ||
} | ||
} | ||
|
||
#[doc(hidden)] | ||
#[derive(Copy, Clone, Debug, PartialEq, Eq)] | ||
pub enum State { | ||
Dolphin(dolphin::State), | ||
Retroarch(retroarch::State), | ||
} | ||
|
||
static PROCESS_NAMES: [(&str, State); 2] = [ | ||
("Dolphin.exe", State::Dolphin(dolphin::State)), | ||
("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,45 @@ | ||
use crate::{file_format::pe, Address, Endian, Process}; | ||
|
||
#[derive(Copy, Clone, Debug, PartialEq, Eq)] | ||
pub struct State { | ||
core_base: Address, | ||
} | ||
|
||
impl State { | ||
pub fn find_ram(&mut self, game: &Process, endian: &mut Endian) -> Option<[Address; 2]> { | ||
const SUPPORTED_CORES: [&str; 1] = ["dolphin_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 Dolphin core, the only one available for retroarch, only supports 64-bit | ||
return None; | ||
} | ||
|
||
self.core_base = SUPPORTED_CORES | ||
.iter() | ||
.find_map(|&m| game.get_module_address(m).ok())?; | ||
|
||
*endian = Endian::Big; | ||
super::dolphin::State::find_ram(&super::dolphin::State, game, endian) | ||
} | ||
|
||
pub fn keep_alive(&self, game: &Process, ram_base: &Option<[Address; 2]>) -> bool { | ||
game.read::<u8>(self.core_base).is_ok() | ||
&& ram_base.is_some_and(|[mem1, mem2]| { | ||
game.read::<u8>(mem1).is_ok() && game.read::<u8>(mem2).is_ok() | ||
}) | ||
} | ||
|
||
pub const fn new() -> Self { | ||
Self { | ||
core_base: Address::NULL, | ||
} | ||
} | ||
} |