Merge commit '6889711f5bff710ee36a295fd5126d9e4221ca2b'
This commit is contained in:
commit
f5d5bb1b08
|
@ -8,7 +8,21 @@
|
|||
"name": "sync: pokeblue",
|
||||
"type": "cppvsdbg",
|
||||
"request": "launch",
|
||||
"program": "target/debug/examples/sdl2_emulator.exe",
|
||||
"program": "target/debug/ferretro-synced.exe",
|
||||
"args": ["--core", "./data/gambatte_libretro.dll", "--rom", "./data/pokeblue.gb", "--state", "./data/pkblue-route1.sav"],
|
||||
"stopAtEntry": false,
|
||||
"cwd": "${workspaceFolder}",
|
||||
"environment": [{
|
||||
"name": "RUST_BACKTRACE",
|
||||
"value": "full"
|
||||
}],
|
||||
"externalConsole": false,
|
||||
},
|
||||
{
|
||||
"name": "sync: pokeblue 2",
|
||||
"type": "cppvsdbg",
|
||||
"request": "launch",
|
||||
"program": "target/debug/ferretro-synced.exe",
|
||||
"args": ["--core", "./data/gambatte_libretro.dll", "--rom", "./data/pokeblue.gb", "--state", "./data/pkblue-route1.sav"],
|
||||
"stopAtEntry": false,
|
||||
"cwd": "${workspaceFolder}",
|
||||
|
@ -22,7 +36,7 @@
|
|||
"name": "sync: sonic2",
|
||||
"type": "cppvsdbg",
|
||||
"request": "launch",
|
||||
"program": "target/debug/examples/sdl2_emulator.exe",
|
||||
"program": "target/debug/ferretro-synced.exe",
|
||||
"args": ["--core", "./data/genesis_plus_gx_libretro.dll", "--rom", "./data/sonic2.bin"],
|
||||
"stopAtEntry": false,
|
||||
"cwd": "${workspaceFolder}",
|
||||
|
|
File diff suppressed because it is too large
Load Diff
12
Cargo.toml
12
Cargo.toml
|
@ -14,9 +14,13 @@ libloading = "^0.5"
|
|||
num = "0.2"
|
||||
num-derive = "0.3"
|
||||
num-traits = "0.2"
|
||||
|
||||
[dev-dependencies]
|
||||
# example: sdl2_emulator
|
||||
sdl2 = "^0.32"
|
||||
crossbeam-channel = "^0.4"
|
||||
structopt = "^0.3"
|
||||
structopt = "^0.3"
|
||||
serde = { version = "1.0.104", features = ["derive"] }
|
||||
serde_bytes = "0.11"
|
||||
serde_json = "1.0.44"
|
||||
websocket = "0.24.0"
|
||||
ascii = "1.0.0"
|
||||
|
||||
[dev-dependencies]
|
|
@ -1,25 +1,20 @@
|
|||
extern crate crossbeam_channel;
|
||||
extern crate ferretro;
|
||||
extern crate sdl2;
|
||||
|
||||
use ferretro::retro;
|
||||
use ferretro::retro::ffi::{GameGeometry, SystemInfo, SystemAvInfo, MemoryMap};
|
||||
use ferretro::retro::constants::{InputIndex, JoypadButton, AnalogAxis, DeviceType};
|
||||
use ferretro::retro::wrapped_types::{ControllerDescription2, InputDescriptor2, InputDeviceId, SubsystemInfo2, Variable2};
|
||||
use ferretro::retro::wrapper::LibretroWrapper;
|
||||
|
||||
use ferretro_synced::sync::memory_rw::MemoryRw;
|
||||
use ferretro_synced::sync::pokemon_rb::SyncedPokemonRedBlue;
|
||||
use crate::sync::game::{SyncableGame, KnownGames, read_game_name, create_game};
|
||||
use crate::emulator::libretro_memory_map::LibRetroMemoryMap;
|
||||
use crate::emulator::metadata_reader::{GameInfo, get_game_info};
|
||||
|
||||
use std::ffi::CStr;
|
||||
use std::io::Read;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::pin::Pin;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::{Duration, Instant};
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use structopt::StructOpt;
|
||||
|
||||
use sdl2::audio::{AudioCallback, AudioFormat, AudioSpec, AudioSpecDesired, AudioDevice};
|
||||
use sdl2::controller::{GameController, Button, Axis};
|
||||
use sdl2::event::Event;
|
||||
|
@ -27,7 +22,7 @@ use sdl2::keyboard::Keycode;
|
|||
use sdl2::rect::Rect;
|
||||
use sdl2::render::WindowCanvas;
|
||||
|
||||
struct MyEmulator {
|
||||
pub struct MyEmulator {
|
||||
retro: retro::wrapper::LibretroWrapper,
|
||||
core_path: PathBuf,
|
||||
sys_path: Option<PathBuf>,
|
||||
|
@ -36,6 +31,7 @@ struct MyEmulator {
|
|||
|
||||
sys_info: SystemInfo,
|
||||
av_info: SystemAvInfo,
|
||||
pub game_info: Option<GameInfo>,
|
||||
|
||||
sdl_context: sdl2::Sdl,
|
||||
|
||||
|
@ -54,10 +50,8 @@ struct MyEmulator {
|
|||
gamepads: Vec<GameController>,
|
||||
pressed_keys: Vec<Keycode>,
|
||||
|
||||
// memory
|
||||
memory_map: Vec<MyMemoryMap>,
|
||||
|
||||
synced_pokemonrb: SyncedPokemonRedBlue,
|
||||
pub memory_map: Option<LibRetroMemoryMap>,
|
||||
synced_game: Option<Box<dyn SyncableGame>>,
|
||||
}
|
||||
|
||||
impl MyEmulator {
|
||||
|
@ -124,8 +118,11 @@ impl MyEmulator {
|
|||
|
||||
let pressed_keys = Vec::new();
|
||||
|
||||
let memory_map = Vec::new();
|
||||
let synced_pokemonrb = std::default::Default::default();
|
||||
/*
|
||||
let memory_map = std::default::Default::default();
|
||||
let which_game = read_game_name("whatever");
|
||||
let synced_pokemonrb = SyncedPokemonRedBlue::create(memory_map);
|
||||
*/
|
||||
|
||||
let emu = MyEmulator {
|
||||
retro,
|
||||
|
@ -134,6 +131,7 @@ impl MyEmulator {
|
|||
preferred_pad: None,
|
||||
av_info,
|
||||
sys_info,
|
||||
game_info: None,
|
||||
sdl_context,
|
||||
canvas,
|
||||
pixel_format,
|
||||
|
@ -144,8 +142,8 @@ impl MyEmulator {
|
|||
gamepad_subsys,
|
||||
gamepads,
|
||||
pressed_keys,
|
||||
memory_map,
|
||||
synced_pokemonrb,
|
||||
memory_map: None,
|
||||
synced_game: None,
|
||||
};
|
||||
let mut pin_emu = Box::pin(emu);
|
||||
retro::wrapper::set_handler(pin_emu.as_mut());
|
||||
|
@ -153,6 +151,10 @@ impl MyEmulator {
|
|||
pin_emu
|
||||
}
|
||||
|
||||
pub fn begin_sync(&mut self, synced_game: Box<dyn SyncableGame>){
|
||||
self.synced_game = Some(synced_game);
|
||||
}
|
||||
|
||||
pub fn run(&mut self) {
|
||||
self.audio_device.resume();
|
||||
let mut event_pump = self.sdl_context.event_pump().unwrap();
|
||||
|
@ -172,6 +174,18 @@ impl MyEmulator {
|
|||
|
||||
self.update_key_state(&event_pump.keyboard_state());
|
||||
|
||||
match &mut self.synced_game {
|
||||
Some(g) => {
|
||||
match g.handle_inbound_messages() {
|
||||
Ok(_) => (),
|
||||
Err(e) => {
|
||||
println!("Error while handling inbound messages");
|
||||
}
|
||||
}
|
||||
},
|
||||
None => ()
|
||||
}
|
||||
|
||||
// The rest of the game loop goes here...
|
||||
self.retro.run();
|
||||
self.canvas.present();
|
||||
|
@ -182,8 +196,14 @@ impl MyEmulator {
|
|||
spf = 1.0 / 60.0;
|
||||
}
|
||||
|
||||
let new_data = SyncedPokemonRedBlue::copy_from_emu(self);
|
||||
std::mem::replace(&mut self.synced_pokemonrb, new_data);
|
||||
match &mut self.synced_game {
|
||||
Some(g) => {
|
||||
g.update_state();
|
||||
g.send_state_messages();
|
||||
},
|
||||
None => ()
|
||||
}
|
||||
//println!("{}", self.synced_pokemonrb);
|
||||
|
||||
Duration::from_secs_f64(spf)
|
||||
.checked_sub(frame_begin.elapsed())
|
||||
|
@ -191,17 +211,31 @@ impl MyEmulator {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn load_game(&self, rom: impl AsRef<Path>) {
|
||||
pub fn load_game(&mut self, rom: impl AsRef<Path>) {
|
||||
let path = rom.as_ref();
|
||||
let mut data = None;
|
||||
let mut v = Vec::new();
|
||||
if !self.sys_info.need_fullpath {
|
||||
if let Ok(mut f) = std::fs::File::open(path) {
|
||||
if f.read_to_end(&mut v).is_ok() {
|
||||
data = Some(v.as_ref());
|
||||
}
|
||||
|
||||
if let Ok(mut f) = std::fs::File::open(path) {
|
||||
if f.read_to_end(&mut v).is_ok() {
|
||||
data = Some(v.as_ref());
|
||||
|
||||
let library_name = unsafe { CStr::from_ptr(self.sys_info.library_name) }.to_string_lossy();
|
||||
|
||||
self.game_info = match get_game_info(library_name.as_ref(), v.as_ref()) {
|
||||
Ok(game_info) => Some(game_info),
|
||||
Err(e) => {
|
||||
println!("Couldn't get game metadata: {:?}", e);
|
||||
None
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if self.sys_info.need_fullpath { // this core will load the rom itself, so don't pass it data
|
||||
data = None
|
||||
}
|
||||
|
||||
self.retro
|
||||
.load_game(Some(path), data, None)
|
||||
.unwrap();
|
||||
|
@ -279,26 +313,6 @@ impl MyEmulator {
|
|||
}
|
||||
|
||||
|
||||
fn find_bank(&self, offset: usize, length: usize, bank_switch: usize) -> Option<*const std::ffi::c_void> {
|
||||
if self.memory_map.len() == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
for item in self.memory_map.iter() {
|
||||
if (item.start <= offset) && (offset < item.end) {
|
||||
if offset + length > item.end {
|
||||
println!("({:x}, {:x}) overruns ({:x}, {:x}) memory bank.", offset, length, item.start, item.end);
|
||||
return None;
|
||||
}
|
||||
|
||||
let bank_size = item.end - item.start;
|
||||
let relative_offset = isize::try_from(offset - item.start + (bank_size * bank_switch)).unwrap();
|
||||
return unsafe { Some(item.pointer.offset(relative_offset)) }
|
||||
}
|
||||
}
|
||||
println!("({:x}, {:x}) address range not found in any memory map region.", offset, length);
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for MyEmulator {
|
||||
|
@ -435,50 +449,12 @@ impl retro::wrapper::Handler for MyEmulator {
|
|||
eprint!("[{:?}] {}", level, msg);
|
||||
}
|
||||
|
||||
fn set_memory_maps(&mut self, memory_map: MemoryMap) -> bool {
|
||||
let mut descs: Vec<MyMemoryMap> = Vec::new();
|
||||
let num_descriptors = isize::try_from(memory_map.num_descriptors).unwrap();
|
||||
for i in 0..num_descriptors {
|
||||
let desc = unsafe { &*memory_map.descriptors.offset(i) };
|
||||
let mut length = desc.len;
|
||||
|
||||
if desc.select > 0 { // FIXME: hack for oversized SRAM eating addr space
|
||||
length = (!desc.select + 1) & 0xffffffff;
|
||||
println!("truncating memory region {:x} from size {:x} to {:x} banks of size {:x}", desc.start, desc.len, desc.len/length, length)
|
||||
}
|
||||
|
||||
descs.push(MyMemoryMap {
|
||||
start: desc.start,
|
||||
end: desc.start + length,
|
||||
pointer: desc.ptr,
|
||||
})
|
||||
}
|
||||
|
||||
self.memory_map = descs;
|
||||
|
||||
fn set_memory_maps(&mut self, ffi_memory_map: MemoryMap) -> bool {
|
||||
self.memory_map = Some(LibRetroMemoryMap::create(&ffi_memory_map));
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl MemoryRw for MyEmulator {
|
||||
fn peek_region<T>(&self, offset: usize, bank_switch: usize) -> Option<&T> {
|
||||
let length = std::mem::size_of::<T>();
|
||||
match self.find_bank(offset, length, bank_switch) {
|
||||
Some(ptr) => Some(unsafe { &*(ptr as *mut T) }),
|
||||
None => None
|
||||
}
|
||||
}
|
||||
|
||||
fn poke_region<T>(&mut self, offset: usize, data: T, bank_switch: usize) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
struct MyMemoryMap {
|
||||
start: usize,
|
||||
end: usize,
|
||||
pointer: *const std::ffi::c_void,
|
||||
}
|
||||
|
||||
struct MySdlAudio {
|
||||
audio_spec: AudioSpec,
|
||||
|
@ -497,34 +473,6 @@ impl AudioCallback for MySdlAudio {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn main() -> failure::Fallible<()> {
|
||||
let opt: Opt = Opt::from_args();
|
||||
let mut emu = MyEmulator::new(&opt.core, &opt.system);
|
||||
emu.load_game(&opt.rom);
|
||||
match (&opt.state) {
|
||||
Some(s) => emu.load_state(s),
|
||||
None => ()
|
||||
}
|
||||
emu.run();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(StructOpt)]
|
||||
struct Opt {
|
||||
/// Core module to use.
|
||||
#[structopt(short, long, parse(from_os_str))]
|
||||
core: PathBuf,
|
||||
/// ROM to load using the core.
|
||||
#[structopt(short, long, parse(from_os_str))]
|
||||
rom: PathBuf,
|
||||
/// System directory, often containing BIOS files
|
||||
#[structopt(short, long, parse(from_os_str))]
|
||||
system: Option<PathBuf>,
|
||||
/// Serialized game state to load
|
||||
#[structopt(long, parse(from_os_str))]
|
||||
state: Option<PathBuf>,
|
||||
}
|
||||
|
||||
fn button_map(retro_button: &JoypadButton) -> Option<Button> {
|
||||
match retro_button {
|
||||
|
@ -576,4 +524,4 @@ fn axis_map(index: &InputIndex, axis: &AnalogAxis) -> Axis {
|
|||
(InputIndex::Right, AnalogAxis::X) => Axis::RightX,
|
||||
(InputIndex::Right, AnalogAxis::Y) => Axis::RightY,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
use std::convert::TryFrom;
|
||||
use ferretro::retro::ffi::{MemoryMap};
|
||||
|
||||
pub struct LibRetroMemoryMap {
|
||||
regions: Vec<LibRetroMemoryRegion>,
|
||||
}
|
||||
|
||||
impl LibRetroMemoryMap {
|
||||
pub fn get_slice_from_region(&self, offset: usize, length: usize, bank_switch: usize) -> Option<&'static mut [u8]> {
|
||||
match self.find_bank(offset, length, bank_switch) {
|
||||
Some(ptr) => Some(unsafe { std::slice::from_raw_parts_mut(ptr as *mut u8, length) }),
|
||||
None => None
|
||||
}
|
||||
}
|
||||
|
||||
fn find_bank(&self, offset: usize, length: usize, bank_switch: usize) -> Option<*const std::ffi::c_void> {
|
||||
if self.regions.len() == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
for item in self.regions.iter() {
|
||||
if (item.start <= offset) && (offset < item.end) {
|
||||
if offset + length > item.end {
|
||||
println!("({:x}, {:x}) overruns ({:x}, {:x}) memory bank.", offset, length, item.start, item.end);
|
||||
return None;
|
||||
}
|
||||
|
||||
let bank_size = item.end - item.start;
|
||||
let relative_offset = isize::try_from(offset - item.start + (bank_size * bank_switch)).unwrap();
|
||||
return unsafe { Some(item.pointer.offset(relative_offset)) }
|
||||
}
|
||||
}
|
||||
println!("({:x}, {:x}) address range not found in any memory map region.", offset, length);
|
||||
None
|
||||
}
|
||||
|
||||
pub fn create(memory_map: &MemoryMap) -> Self {
|
||||
let mut regions: Vec<LibRetroMemoryRegion> = Vec::new();
|
||||
let num_descriptors = isize::try_from(memory_map.num_descriptors).unwrap();
|
||||
for i in 0..num_descriptors {
|
||||
let region = unsafe { &*memory_map.descriptors.offset(i) };
|
||||
let mut length = region.len;
|
||||
|
||||
if region.select > 0 { // FIXME: hack for oversized SRAM eating addr space
|
||||
length = (!region.select + 1) & 0xffffffff;
|
||||
println!("truncating memory region {:x} from size {:x} to {:x} banks of size {:x}", region.start, region.len, region.len/length, length)
|
||||
}
|
||||
|
||||
regions.push(LibRetroMemoryRegion {
|
||||
start: region.start,
|
||||
end: region.start + length,
|
||||
pointer: region.ptr,
|
||||
})
|
||||
}
|
||||
|
||||
LibRetroMemoryMap {
|
||||
regions: regions
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct LibRetroMemoryRegion {
|
||||
start: usize,
|
||||
end: usize,
|
||||
pointer: *const std::ffi::c_void,
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
use ascii::AsAsciiStr;
|
||||
|
||||
pub enum GamePlatform {
|
||||
GameBoyColor,
|
||||
SuperNintendo,
|
||||
Genesis
|
||||
}
|
||||
|
||||
pub struct GameInfo {
|
||||
pub platform: GamePlatform,
|
||||
pub core_name: String,
|
||||
pub game_name: String,
|
||||
}
|
||||
|
||||
fn core_to_platform(core_name: &str) -> GamePlatform {
|
||||
match core_name.to_lowercase().as_str() {
|
||||
"gambatte" => GamePlatform::GameBoyColor,
|
||||
"bsnes" => GamePlatform::SuperNintendo,
|
||||
"genesis plus gx" => GamePlatform::Genesis,
|
||||
"higan (super famicom accuracy)" => GamePlatform::SuperNintendo,
|
||||
_ => panic!(format!("unknown core {}", core_name))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_game_info(core_name: &str, data: &[u8]) -> Result<GameInfo, failure::Error> {
|
||||
let platform = core_to_platform(&core_name);
|
||||
|
||||
let game_name = match &platform {
|
||||
GamePlatform::GameBoyColor => data.slice_ascii(0x134..0x143)?.to_string()
|
||||
.trim_end_matches("\0").to_string(), // remove nulls at end
|
||||
|
||||
GamePlatform::SuperNintendo => data.slice_ascii(0x7fc0..0x7fd5)?.to_string()
|
||||
.trim_end_matches("\0").to_string(), // remove nulls at end
|
||||
|
||||
GamePlatform::Genesis => data.slice_ascii(0x120..0x150)?.to_string()
|
||||
.split(" ")
|
||||
.filter(|w| w.len() > 0)
|
||||
.collect::<Vec<&str>>()
|
||||
.join(" "), // remove duplicate spaces
|
||||
};
|
||||
|
||||
Ok(GameInfo {
|
||||
platform,
|
||||
core_name: String::from(core_name),
|
||||
game_name,
|
||||
})
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
pub mod emulator;
|
||||
pub mod libretro_memory_map;
|
||||
pub mod metadata_reader;
|
|
@ -4,4 +4,5 @@ extern crate num;
|
|||
#[macro_use]
|
||||
extern crate num_derive;
|
||||
|
||||
pub mod sync;
|
||||
pub mod sync;
|
||||
pub mod emulator;
|
|
@ -0,0 +1,76 @@
|
|||
#[macro_use] extern crate failure;
|
||||
extern crate crossbeam_channel;
|
||||
extern crate ferretro;
|
||||
extern crate sdl2;
|
||||
extern crate serde;
|
||||
extern crate serde_bytes;
|
||||
extern crate ascii;
|
||||
|
||||
use structopt::StructOpt;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use ferretro_synced::emulator::emulator::MyEmulator;
|
||||
use ferretro_synced::emulator::libretro_memory_map::LibRetroMemoryMap;
|
||||
use ferretro_synced::sync::game::{SyncableGame, KnownGames, read_game_name, create_game};
|
||||
use ferretro_synced::sync::comms::CommunicationSettings;
|
||||
|
||||
pub fn main() -> failure::Fallible<()> {
|
||||
let opt: Opt = Opt::from_args();
|
||||
let mut emu = MyEmulator::new(&opt.core, &opt.system);
|
||||
emu.load_game(&opt.rom);
|
||||
|
||||
// memory map should be ready after init & loading game.
|
||||
//let memory_map = emu.memory_map.as_ref().expect("Memory map was not set by emulator core, cannot continue.");
|
||||
|
||||
match (&opt.state) {
|
||||
Some(s) => emu.load_state(s),
|
||||
None => ()
|
||||
}
|
||||
|
||||
let comms_settings = CommunicationSettings {
|
||||
connection: "ws://127.0.0.1:8765".to_string()
|
||||
};
|
||||
|
||||
|
||||
if let Err(e) = attempt_to_start_sync(&mut emu, comms_settings) {
|
||||
println!("Error trying to start sync: {:?}", e);
|
||||
}
|
||||
|
||||
emu.run();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn attempt_to_start_sync(emu: &mut MyEmulator, comms_settings: CommunicationSettings) -> Result<(), failure::Error> {
|
||||
match &emu.game_info {
|
||||
Some(game_info) => match read_game_name(&game_info.game_name) {
|
||||
Some(which_game) => match &emu.memory_map {
|
||||
Some(memory_map) => {
|
||||
let synced_game = create_game(which_game, comms_settings, memory_map);
|
||||
emu.begin_sync(synced_game);
|
||||
Ok(())
|
||||
},
|
||||
None => Err(format_err!("Memory map has not been set"))
|
||||
},
|
||||
None => Err(format_err!("Unrecognized game {}", game_info.game_name))
|
||||
},
|
||||
None => Err(format_err!("Couldn't get game metadata"))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#[derive(StructOpt)]
|
||||
struct Opt {
|
||||
/// Core module to use.
|
||||
#[structopt(short, long, parse(from_os_str))]
|
||||
core: PathBuf,
|
||||
/// ROM to load using the core.
|
||||
#[structopt(short, long, parse(from_os_str))]
|
||||
rom: PathBuf,
|
||||
/// System directory, often containing BIOS files
|
||||
#[structopt(short, long, parse(from_os_str))]
|
||||
system: Option<PathBuf>,
|
||||
/// Serialized game state to load
|
||||
#[structopt(long, parse(from_os_str))]
|
||||
state: Option<PathBuf>,
|
||||
}
|
|
@ -0,0 +1,140 @@
|
|||
use std::path::{Path, PathBuf};
|
||||
use std::sync::mpsc::{Sender, Receiver, TryRecvError};
|
||||
use std::sync::mpsc;
|
||||
use std::thread;
|
||||
use std::thread::JoinHandle;
|
||||
use serde::Serialize;
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde_json;
|
||||
|
||||
use websocket::client::ClientBuilder;
|
||||
use websocket::receiver::Reader;
|
||||
use websocket::sender::Writer;
|
||||
use websocket::{Message, OwnedMessage};
|
||||
|
||||
const CONNECTION: &'static str = "ws://127.0.0.1:8765";
|
||||
pub struct Communication<T> {
|
||||
rx: Receiver<T>,
|
||||
tx: Sender<OwnedMessage>,
|
||||
inbound_thread: JoinHandle<Result<(), failure::Error>>,
|
||||
outbound_thread: JoinHandle<Result<(), failure::Error>>,
|
||||
}
|
||||
|
||||
pub struct CommunicationSettings {
|
||||
pub connection: String
|
||||
}
|
||||
|
||||
impl<T> Communication<T> where T: std::marker::Send, T: Serialize, T: DeserializeOwned, T: 'static {
|
||||
pub fn new(settings: CommunicationSettings) -> Self {
|
||||
let (tx, from_main): (Sender<OwnedMessage>, Receiver<OwnedMessage>) = mpsc::channel();
|
||||
let (to_main, rx): (Sender<T>, Receiver<T>) = mpsc::channel();
|
||||
|
||||
// transmitter for ping and close messages
|
||||
let tx_1 = tx.clone();
|
||||
|
||||
let ws_client = ClientBuilder::new(settings.connection.as_str())
|
||||
.unwrap()
|
||||
.add_protocol("rust-websocket")
|
||||
.connect_insecure()
|
||||
.unwrap();
|
||||
|
||||
let (mut ws_receiver, mut ws_sender) = ws_client.split().unwrap();
|
||||
|
||||
let outbound = thread::spawn(move || -> Result<(), failure::Error> {
|
||||
let mut from_main = from_main;
|
||||
let mut ws_sender = ws_sender;
|
||||
let mut flush = || -> Result<(), failure::Error> {
|
||||
let msg = from_main.recv()?;
|
||||
println!("sending a message {:?}", &msg);
|
||||
ws_sender.send_message(&msg)?;
|
||||
Ok(())
|
||||
};
|
||||
|
||||
loop {
|
||||
match flush() {
|
||||
Ok(_) => (),
|
||||
Err(e) => {
|
||||
println!("Error in outbound thread: {}", e);
|
||||
return Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let inbound = thread::spawn(move || -> Result<(), failure::Error> {
|
||||
let mut to_main = to_main;
|
||||
let mut ws_receiver = ws_receiver;
|
||||
let mut tx_1 = tx_1;
|
||||
|
||||
let mut flush = || -> Result<(), failure::Error> {
|
||||
let msg = ws_receiver.recv_message()?;
|
||||
match msg {
|
||||
OwnedMessage::Text(text) => match serde_json::from_str(&text) {
|
||||
Ok(m) => {
|
||||
println!("receiving a message {}", &text);
|
||||
to_main.send(m); // todo print an error
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => Err(failure::err_msg(format!("deserialization error: {:?}", e)))
|
||||
},
|
||||
OwnedMessage::Binary(_) => Err(failure::err_msg("binary message not implemented")),
|
||||
OwnedMessage::Close(_) => {
|
||||
tx_1.send(OwnedMessage::Close(None));
|
||||
Err(failure::err_msg("Connection closed by request."))
|
||||
}
|
||||
OwnedMessage::Ping(data) => {
|
||||
tx_1.send(OwnedMessage::Pong(data));
|
||||
// todo print an error
|
||||
Ok(())
|
||||
},
|
||||
OwnedMessage::Pong(_) => {
|
||||
println!("received pong");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
loop {
|
||||
match flush() {
|
||||
Ok(_) => (),
|
||||
Err(e) => {
|
||||
println!("Error in inbound thread: {}", e);
|
||||
return Err(e)
|
||||
}
|
||||
}
|
||||
/*
|
||||
match Communication::<T>::handle_inbound(to_main) {
|
||||
Ok(_) => (),
|
||||
Err(e) => return Err(e)
|
||||
}
|
||||
*/
|
||||
}
|
||||
});
|
||||
|
||||
Communication {
|
||||
tx: tx,
|
||||
rx: rx,
|
||||
inbound_thread: inbound,
|
||||
outbound_thread: outbound,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send(&mut self, msg: T) -> Result<(), failure::Error> {
|
||||
let msg_serialized = serde_json::to_string(&msg)?;
|
||||
self.tx.send(OwnedMessage::Text(msg_serialized))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn receive_if_any(&mut self) -> Option<T> {
|
||||
match self.rx.try_recv() {
|
||||
Err(e) => match e {
|
||||
TryRecvError::Empty => None,
|
||||
TryRecvError::Disconnected => {
|
||||
println!("Disconnected when trying to receive.");
|
||||
None
|
||||
}
|
||||
}
|
||||
Ok(msg) => Some(msg)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
use crate::sync::pokemon_rb::SyncedPokemonRedBlue;
|
||||
use crate::sync::comms::CommunicationSettings;
|
||||
use crate::emulator::libretro_memory_map::LibRetroMemoryMap;
|
||||
|
||||
pub trait SyncableGame {
|
||||
// Update internal state based on tracked memory and the emulator
|
||||
fn update_state(&mut self);
|
||||
|
||||
// Check for messages and handle them
|
||||
fn handle_inbound_messages(&mut self) -> Result<(), failure::Error>;
|
||||
|
||||
// Check for messages and handle them
|
||||
fn send_state_messages(&mut self) -> Result<(), failure::Error>;
|
||||
}
|
||||
|
||||
pub enum KnownGames {
|
||||
PokemonRedBlue,
|
||||
}
|
||||
|
||||
pub fn read_game_name(name: &str) -> Option<KnownGames> {
|
||||
match name {
|
||||
"POKEMON BLUE" => Some(KnownGames::PokemonRedBlue),
|
||||
_ => None
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub fn create_game(which_game: KnownGames, comms_settings: CommunicationSettings, memory_map: &LibRetroMemoryMap) -> Box<dyn SyncableGame> {
|
||||
match which_game {
|
||||
KnownGames::PokemonRedBlue => Box::from(SyncedPokemonRedBlue::create(comms_settings, memory_map))
|
||||
}
|
||||
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
pub trait MemoryRw {
|
||||
fn peek_region<T>(&self, offset: usize, bank_switch: usize) -> Option<&T>;
|
||||
fn poke_region<T>(&mut self, offset: usize, data: T, bank_switch: usize);
|
||||
}
|
|
@ -0,0 +1,91 @@
|
|||
use crate::emulator::libretro_memory_map::LibRetroMemoryMap;
|
||||
|
||||
pub struct MemorySliceHandle<'a> {
|
||||
pub offset: usize,
|
||||
pub length: usize,
|
||||
pub bank_switch: usize,
|
||||
pub slice: Option<&'a mut [u8]>,
|
||||
pub last_read_value: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
impl MemorySliceHandle<'_> {
|
||||
pub fn create(offset: usize, length: usize, bank_switch: usize, memory_map: &LibRetroMemoryMap) -> Self {
|
||||
let slice = memory_map.get_slice_from_region(offset, length, bank_switch);
|
||||
|
||||
MemorySliceHandle {
|
||||
offset: offset,
|
||||
length: length,
|
||||
bank_switch: bank_switch,
|
||||
slice,
|
||||
last_read_value: None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write_to_slice(&mut self, data: Vec<u8>) -> Result<(), failure::Error> {
|
||||
match &mut self.slice {
|
||||
Some(slice) => {
|
||||
if data.len() != slice.len() {
|
||||
return Err(failure::err_msg("message size and slice size differ."));
|
||||
}
|
||||
|
||||
// if last_read_value is set, update it as the same time that we write back to the emulator.
|
||||
match &mut self.last_read_value {
|
||||
None => {
|
||||
let mut index: usize = 0;
|
||||
for value in data.into_iter(){
|
||||
slice[index] = value;
|
||||
index += 1;
|
||||
}
|
||||
},
|
||||
Some(last_read_value) => {
|
||||
let mut index: usize = 0;
|
||||
for value in data.into_iter(){
|
||||
slice[index] = value;
|
||||
last_read_value[index] = value;
|
||||
index += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
},
|
||||
None => {
|
||||
println!("slice doesn't exist to write to.");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_copy_if_changed_since_last_read(&mut self) -> Option<Vec<u8>> {
|
||||
match &mut self.slice {
|
||||
None => None,
|
||||
Some(data) => match &mut self.last_read_value {
|
||||
None => {
|
||||
// last read value isn't allocated yet
|
||||
let last = data.to_vec();
|
||||
self.last_read_value = Some(last);
|
||||
self.last_read_value.clone()
|
||||
},
|
||||
Some(last) => {
|
||||
let mut index: usize = 0;
|
||||
let mut any_changes = false;
|
||||
|
||||
for value in data.into_iter() {
|
||||
if last[index] != *value {
|
||||
last[index] = *value;
|
||||
any_changes = true;
|
||||
}
|
||||
index += 1;
|
||||
}
|
||||
|
||||
if any_changes {
|
||||
return Some(last.clone());
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -1,2 +1,4 @@
|
|||
pub mod pokemon_rb;
|
||||
pub mod memory_rw;
|
||||
pub mod comms;
|
||||
pub mod memory_slice_handle;
|
||||
pub mod game;
|
|
@ -1,53 +1,79 @@
|
|||
use std::default::Default;
|
||||
use std::fmt::Display;
|
||||
use crate::sync::memory_rw::MemoryRw;
|
||||
use crate::emulator::libretro_memory_map::LibRetroMemoryMap;
|
||||
use serde::{Serialize, Deserialize};
|
||||
use ferretro::retro::wrapper::LibretroWrapper; // want LibretroWrapper.api : LibretroApi.get_memory()
|
||||
use crate::num::ToPrimitive;
|
||||
use crate::sync::comms::Communication;
|
||||
use crate::sync::comms::CommunicationSettings;
|
||||
use crate::sync::memory_slice_handle::MemorySliceHandle;
|
||||
use crate::sync::game::SyncableGame;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct SyncedPokemonRedBlue {
|
||||
raw: Raw,
|
||||
memory_handles: PokemonRedBlueMemoryHandles<'static>,
|
||||
battle_context: BattleContext,
|
||||
comms: Communication<PokemonRedBlueMessage>
|
||||
}
|
||||
|
||||
struct Raw {
|
||||
active_pokemon_raw: [u8; 0x27],
|
||||
player_and_party: [u8; 0x19e],
|
||||
pokemon_out: u8,
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct PokemonRedBlueMessage {
|
||||
active_pkmn: Option<Vec<u8>>
|
||||
}
|
||||
|
||||
impl From<&mut PokemonRedBlueMemoryHandles<'_>> for PokemonRedBlueMessage {
|
||||
fn from(src: &mut PokemonRedBlueMemoryHandles) -> Self {
|
||||
PokemonRedBlueMessage {
|
||||
active_pkmn: src.active_pokemon.get_copy_if_changed_since_last_read()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct PokemonRedBlueMemoryHandles<'a> {
|
||||
active_pokemon: MemorySliceHandle<'a>,
|
||||
player_and_party: MemorySliceHandle<'a>,
|
||||
pokemon_out: MemorySliceHandle<'a>,
|
||||
}
|
||||
|
||||
struct BattleContext {
|
||||
active_pokemon: Pokemon,
|
||||
|
||||
}
|
||||
|
||||
impl SyncedPokemonRedBlue {
|
||||
pub fn copy_from_emu<T: MemoryRw>(emu: &T) -> SyncedPokemonRedBlue {
|
||||
let raw = Raw {
|
||||
active_pokemon_raw: *emu.peek_region(0xd009, 0).unwrap_or_else(|| &[0; 0x27]),
|
||||
player_and_party: *emu.peek_region(0xd158, 0).unwrap_or_else(|| &[0; 0x19e]),
|
||||
pokemon_out: *emu.peek_region(0xcc2f, 0).unwrap_or(&0),
|
||||
};
|
||||
|
||||
impl SyncableGame for SyncedPokemonRedBlue {
|
||||
fn update_state(&mut self) {
|
||||
let battle_context = BattleContext {
|
||||
active_pokemon: num::FromPrimitive::from_u8(raw.active_pokemon_raw[0xB]).unwrap_or(Pokemon::Unknown)
|
||||
active_pokemon: match &self.memory_handles.active_pokemon.slice {
|
||||
Some(d) => num::FromPrimitive::from_u8(d[0xB]).unwrap_or(Pokemon::Unknown),
|
||||
None => Pokemon::Unknown
|
||||
}
|
||||
};
|
||||
|
||||
let result = SyncedPokemonRedBlue {
|
||||
raw: raw,
|
||||
battle_context: battle_context,
|
||||
};
|
||||
self.battle_context = battle_context;
|
||||
}
|
||||
|
||||
println!("{}", result);
|
||||
result
|
||||
fn send_state_messages(&mut self) -> Result<(), failure::Error>{
|
||||
let message: PokemonRedBlueMessage = (&mut self.memory_handles).into();
|
||||
if !message.active_pkmn.is_none() {
|
||||
self.comms.send(message)
|
||||
}
|
||||
else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_inbound_messages(&mut self) -> Result<(), failure::Error>{
|
||||
match self.comms.receive_if_any() {
|
||||
Some(msg) => self.handle_message(msg),
|
||||
None => Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Raw {
|
||||
fn default() -> Self {
|
||||
Raw {
|
||||
active_pokemon_raw: [0; 0x27],
|
||||
player_and_party: [0; 0x19e],
|
||||
pokemon_out: 0,
|
||||
impl PokemonRedBlueMemoryHandles<'_> {
|
||||
pub fn create(memory_map: &LibRetroMemoryMap) -> Self {
|
||||
PokemonRedBlueMemoryHandles {
|
||||
active_pokemon: MemorySliceHandle::create(0xd009, 0x27, 0, memory_map),
|
||||
player_and_party: MemorySliceHandle::create(0xd158, 0x19e, 0, memory_map),
|
||||
pokemon_out: MemorySliceHandle::create(0xcc2f, 0x1, 0, memory_map),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -62,10 +88,31 @@ impl Default for BattleContext {
|
|||
|
||||
impl Display for SyncedPokemonRedBlue {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "active: pk {:?} {:?}", &self.battle_context.active_pokemon, &self.raw.active_pokemon_raw[0xB])
|
||||
write!(f, "active: pk {:?}", &self.battle_context.active_pokemon)
|
||||
}
|
||||
}
|
||||
#[derive(FromPrimitive, Debug)]
|
||||
|
||||
impl SyncedPokemonRedBlue {
|
||||
pub fn create(comms_settings: CommunicationSettings, memory_map: &LibRetroMemoryMap) -> Self {
|
||||
SyncedPokemonRedBlue {
|
||||
memory_handles: PokemonRedBlueMemoryHandles::create(memory_map),
|
||||
battle_context: BattleContext::default(),
|
||||
comms: Communication::new(comms_settings),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_message(&mut self, msg: PokemonRedBlueMessage) -> Result<(), failure::Error> {
|
||||
match msg.active_pkmn {
|
||||
Some(src) => {
|
||||
println!("message contains a new active pokemon. data: {:?}", &src);
|
||||
self.memory_handles.active_pokemon.write_to_slice(src)
|
||||
}
|
||||
None => Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(FromPrimitive, ToPrimitive, Debug)]
|
||||
enum Pokemon {
|
||||
Unknown = 0x0,
|
||||
Abra = 0x94,
|
||||
|
@ -219,4 +266,44 @@ enum Pokemon {
|
|||
Wigglytuff = 0x65,
|
||||
Zapdos = 0x4B,
|
||||
Zubat = 0x6B,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json;
|
||||
|
||||
fn build_message(em: &MemorySliceHandle) -> PokemonRedBlueMessage {
|
||||
PokemonRedBlueMessage {
|
||||
active_pkmn: match &em.slice {
|
||||
Some(m) => Some(m.to_vec()),
|
||||
None => None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serde_mem() -> Result<(), String> {
|
||||
let mut data: [u8; 5] = [1, 2, 3, 4, 5];
|
||||
let b: &mut [u8] = &mut data;
|
||||
let slice = MemorySliceHandle {
|
||||
offset: 0,
|
||||
length: 0,
|
||||
bank_switch: 0,
|
||||
slice: Some(b),
|
||||
last_read_value: None
|
||||
};
|
||||
|
||||
let message = build_message(&slice);
|
||||
|
||||
let serialized = serde_json::to_string(&message).unwrap();
|
||||
|
||||
println!("serialized data: {}", serialized);
|
||||
|
||||
let deserialized: PokemonRedBlueMessage = serde_json::from_str(&serialized).unwrap();
|
||||
|
||||
assert_eq!(deserialized.active_pkmn.unwrap()[0], data[0]);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue