From ada4b7e3b4673fc78b03bebfa8be302655470d6d Mon Sep 17 00:00:00 2001 From: Logan McNaughton <848146+loganmc10@users.noreply.github.com> Date: Mon, 20 Jan 2025 21:29:46 +0100 Subject: [PATCH] Netplay registration (#166) * Register player * more * more * more * more * more --- src/device.rs | 11 +-- src/main.rs | 8 +- src/netplay.rs | 89 +++++++++++++++++++++++ src/ui/gui.rs | 69 +++++++++++++----- src/ui/gui/{netplay.rs => gui_netplay.rs} | 55 +++++++++++--- 5 files changed, 197 insertions(+), 35 deletions(-) create mode 100644 src/netplay.rs rename src/ui/gui/{netplay.rs => gui_netplay.rs} (93%) diff --git a/src/device.rs b/src/device.rs index 2a82d96..a89afbf 100644 --- a/src/device.rs +++ b/src/device.rs @@ -10,6 +10,7 @@ include!(concat!(env!("OUT_DIR"), "/simd_bindings.rs")); #[cfg(target_arch = "x86_64")] use std::arch::x86_64::*; +use crate::netplay; use crate::ui; use std::collections::HashMap; use std::fs; @@ -47,17 +48,11 @@ pub mod unmapped; pub mod vi; pub fn run_game( - file_path: &std::path::Path, + rom_contents: Vec, data_dir: std::path::PathBuf, device: &mut Device, fullscreen: bool, ) { - let rom_contents = get_rom_contents(file_path); - if rom_contents.is_empty() { - println!("Could not read rom file"); - return; - } - cart_rom::init(device, rom_contents); // cart needs to come before rdram // rdram pointer is shared with parallel-rdp @@ -166,6 +161,7 @@ pub fn get_rom_contents(file_path: &std::path::Path) -> Vec { } pub struct Device { + pub netplay: Option, pub ui: ui::Ui, byte_swap: usize, cpu: cpu::Cpu, @@ -195,6 +191,7 @@ impl Device { byte_swap = 3; } Device { + netplay: None, ui: ui::Ui::new(config_dir), byte_swap, cpu: cpu::Cpu { diff --git a/src/main.rs b/src/main.rs index a69baff..1d2729e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,6 +4,7 @@ static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc; mod device; +mod netplay; mod ui; use clap::Parser; @@ -156,7 +157,12 @@ async fn main() { if args.game.is_some() { let file_path = std::path::Path::new(args.game.as_ref().unwrap()); - device::run_game(file_path, data_dir, &mut device, args.fullscreen); + let rom_contents = device::get_rom_contents(file_path); + if rom_contents.is_empty() { + println!("Could not read rom file"); + return; + } + device::run_game(rom_contents, data_dir, &mut device, args.fullscreen); } } else { let options = eframe::NativeOptions { diff --git a/src/netplay.rs b/src/netplay.rs new file mode 100644 index 0000000..10ab273 --- /dev/null +++ b/src/netplay.rs @@ -0,0 +1,89 @@ +use crate::device; +use crate::ui; +use std::io::{Read, Write}; +//UDP packet formats +//const UDP_SEND_KEY_INFO: u8 = 0; +//const UDP_RECEIVE_KEY_INFO: u8 = 1; +//const UDP_REQUEST_KEY_INFO: u8 = 2; +//const UDP_RECEIVE_KEY_INFO_GRATUITOUS: u8 = 3; +//const UDP_SYNC_DATA: u8 = 4; + +//TCP packet formats +//const TCP_SEND_SAVE: u8 = 1; +//const TCP_RECEIVE_SAVE: u8 = 2; +//const TCP_SEND_SETTINGS: u8 = 3; +//const TCP_RECEIVE_SETTINGS: u8 = 4; +const TCP_REGISTER_PLAYER: u8 = 5; +const TCP_GET_REGISTRATION: u8 = 6; +const TCP_DISCONNECT_NOTICE: u8 = 7; + +pub struct Netplay { + // udp_socket: std::net::UdpSocket, + tcp_stream: std::net::TcpStream, + player_number: u8, +} + +pub fn init( + mut peer_addr: std::net::SocketAddr, + session: ui::gui::gui_netplay::NetplayRoom, + player_number: u8, +) -> Netplay { + peer_addr.set_port(session.port.unwrap() as u16); + let udp_socket = if peer_addr.is_ipv4() { + std::net::UdpSocket::bind((std::net::Ipv4Addr::UNSPECIFIED, 0)) + .expect("couldn't bind to address") + } else { + std::net::UdpSocket::bind((std::net::Ipv6Addr::UNSPECIFIED, 0)) + .expect("couldn't bind to address") + }; + udp_socket.connect(peer_addr).unwrap(); + + let mut stream = std::net::TcpStream::connect(peer_addr).unwrap(); + + let regid = (player_number + 1) as u32; + let mut request: [u8; 8] = [ + TCP_REGISTER_PLAYER, + player_number, + 0, //plugin/pak + 0, //rawdata + 0, //regid (u32) + 0, //regid (u32) + 0, //regid (u32) + 0, //regid (u32) + ]; + + request[4..8].copy_from_slice(®id.to_be_bytes()); + stream.write_all(&request).unwrap(); + + let mut response: [u8; 2] = [0, 0]; + stream.read_exact(&mut response).unwrap(); + if response[0] != 1 { + panic!("Failed to register player"); + } + let _buffer_target = response[1]; + + let request: [u8; 1] = [TCP_GET_REGISTRATION]; + stream.write_all(&request).unwrap(); + let mut response: [u8; 24] = [0; 24]; + stream.read_exact(&mut response).unwrap(); + + let mut reg_id: [u32; 4] = [0; 4]; + for i in 0..4 { + // reg_id of 0 means no player connected + reg_id[i] = u32::from_be_bytes(response[(i * 6)..(i * 6) + 4].try_into().unwrap()); + } + Netplay { + // udp_socket, + tcp_stream: stream, + player_number, + } +} + +pub fn close(device: &mut device::Device) { + let netplay = device.netplay.as_mut().unwrap(); + let regid = (netplay.player_number + 1) as u32; + let mut request: [u8; 5] = [TCP_DISCONNECT_NOTICE, 0, 0, 0, 0]; + request[1..5].copy_from_slice(®id.to_be_bytes()); + + netplay.tcp_stream.write_all(&request).unwrap(); +} diff --git a/src/ui/gui.rs b/src/ui/gui.rs index 038edca..cd675a7 100644 --- a/src/ui/gui.rs +++ b/src/ui/gui.rs @@ -1,8 +1,8 @@ use crate::device; +use crate::netplay; use crate::ui; use eframe::egui; - -mod netplay; +pub mod gui_netplay; pub struct GopherEguiApp { config_dir: std::path::PathBuf, @@ -24,7 +24,7 @@ pub struct GopherEguiApp { vru_window_receiver: Option>>, vru_word_notifier: Option>, vru_word_list: Vec, - netplay: netplay::Netplay, + pub netplay: gui_netplay::GuiNetplay, } struct SaveConfig { @@ -229,18 +229,38 @@ fn show_vru_dialog(app: &mut GopherEguiApp, ctx: &egui::Context) { ); } -fn open_rom(app: &mut GopherEguiApp, ctx: &egui::Context) { - let task = rfd::AsyncFileDialog::new().pick_file(); +pub fn open_rom(app: &mut GopherEguiApp, ctx: &egui::Context) { + let task; + let netplay; + let selected_controller = app.selected_controller; let selected_profile = app.selected_profile.clone(); let controller_enabled = app.controller_enabled; let upscale = app.upscale; let integer_scaling = app.integer_scaling; let fullscreen = app.fullscreen; - let emulate_vru = app.emulate_vru; + let mut emulate_vru = app.emulate_vru; let config_dir = app.config_dir.clone(); let cache_dir = app.cache_dir.clone(); let data_dir = app.data_dir.clone(); + let peer_addr; + let session; + let player_number; + + if app.netplay.player_name.is_empty() { + task = Some(rfd::AsyncFileDialog::new().pick_file()); + netplay = false; + peer_addr = None; + session = None; + player_number = None; + } else { + task = None; + netplay = true; + emulate_vru = false; + peer_addr = app.netplay.peer_addr; + session = app.netplay.waiting_session.clone(); + player_number = Some(app.netplay.player_number); + } let (vru_window_notifier, vru_window_receiver): ( tokio::sync::mpsc::Sender>, @@ -260,13 +280,15 @@ fn open_rom(app: &mut GopherEguiApp, ctx: &egui::Context) { app.vru_word_notifier = None; } + let rom_contents = app.netplay.rom_contents.clone(); let gui_ctx = ctx.clone(); tokio::spawn(async move { - let file = task.await; + let file = if !netplay { task.unwrap().await } else { None }; - if let Some(file) = file { + if file.is_some() || netplay { let running_file = cache_dir.join("game_running"); if running_file.exists() { + println!("Game already running"); return; } let result = std::fs::File::create(running_file.clone()); @@ -291,12 +313,23 @@ fn open_rom(app: &mut GopherEguiApp, ctx: &egui::Context) { device.vru.word_receiver = Some(vru_word_receiver); device.vru.gui_ctx = Some(gui_ctx); } - device::run_game( - std::path::Path::new(file.path()), - data_dir, - &mut device, - fullscreen, - ); + + if netplay { + device.netplay = Some(netplay::init( + peer_addr.unwrap(), + session.unwrap(), + player_number.unwrap(), + )); + device::run_game(rom_contents, data_dir, &mut device, fullscreen); + netplay::close(&mut device); + } else { + let rom_contents = device::get_rom_contents(file.unwrap().path()); + if rom_contents.is_empty() { + println!("Could not read rom file"); + } else { + device::run_game(rom_contents, data_dir, &mut device, fullscreen); + } + } let result = std::fs::remove_file(running_file.clone()); if result.is_err() { panic!("could not remove running file: {}", result.err().unwrap()) @@ -308,19 +341,19 @@ fn open_rom(app: &mut GopherEguiApp, ctx: &egui::Context) { impl eframe::App for GopherEguiApp { fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { if self.netplay.create { - netplay::netplay_create(self, ctx); + gui_netplay::netplay_create(self, ctx); } if self.netplay.join { - netplay::netplay_join(self, ctx); + gui_netplay::netplay_join(self, ctx); } if self.netplay.wait { - netplay::netplay_wait(self, ctx); + gui_netplay::netplay_wait(self, ctx); } if !self.netplay.error.is_empty() { - netplay::netplay_error(self, ctx, self.netplay.error.clone()); + gui_netplay::netplay_error(self, ctx, self.netplay.error.clone()); } if self.configure_profile { diff --git a/src/ui/gui/netplay.rs b/src/ui/gui/gui_netplay.rs similarity index 93% rename from src/ui/gui/netplay.rs rename to src/ui/gui/gui_netplay.rs index bb00209..be9802a 100644 --- a/src/ui/gui/netplay.rs +++ b/src/ui/gui/gui_netplay.rs @@ -1,4 +1,5 @@ use crate::device; +use crate::ui::gui; use crate::ui::gui::GopherEguiApp; use eframe::egui; use sha2::{Digest, Sha256}; @@ -6,8 +7,10 @@ use sha2::{Digest, Sha256}; const NETPLAY_VERSION: i32 = 17; const EMU_NAME: &str = "gopher64"; +type GameInfo = (String, String, String, Vec); + #[derive(Default)] -pub struct Netplay { +pub struct GuiNetplay { pub create: bool, pub join: bool, pub wait: bool, @@ -24,19 +27,22 @@ pub struct Netplay { pub chat_message: String, pub selected_session: Option, pub pending_begin: bool, + pub peer_addr: Option, pub motd: String, pub sessions: Vec, + pub rom_contents: Vec, + pub player_number: u8, pub player_names: [String; 4], pub server: (String, String), pub socket_waiting: bool, - pub game_info: (String, String, String), + pub game_info: GameInfo, pub servers: std::collections::HashMap, pub waiting_session: Option, pub socket: Option>>, pub server_receiver: Option>>, - pub game_info_receiver: Option>, + pub game_info_receiver: Option>, pub broadcast_socket: Option, pub broadcast_timer: Option, } @@ -49,7 +55,7 @@ pub struct NetplayRoom { #[serde(rename = "MD5")] md5: Option, game_name: Option, - port: Option, + pub port: Option, } #[derive(serde::Serialize, serde::Deserialize)] @@ -193,15 +199,27 @@ pub fn netplay_create(app: &mut GopherEguiApp, ctx: &egui::Context) { let game_name = std::str::from_utf8(&rom_contents[0x20..0x20 + 0x14]) .unwrap() .to_string(); - let _ = tx.send((hash, game_name, file.file_name())).await; + let _ = tx + .send((hash, game_name, file.file_name(), rom_contents)) + .await; } else { let _ = tx - .send(("".to_string(), "".to_string(), "Invalid ROM".to_string())) + .send(( + "".to_string(), + "".to_string(), + "Invalid ROM".to_string(), + vec![], + )) .await; } } else { let _ = tx - .send(("".to_string(), "".to_string(), "Open ROM".to_string())) + .send(( + "".to_string(), + "".to_string(), + "Open ROM".to_string(), + vec![], + )) .await; } gui_ctx.request_repaint(); @@ -228,6 +246,7 @@ pub fn netplay_create(app: &mut GopherEguiApp, ctx: &egui::Context) { let data = result.unwrap(); if !data.0.is_empty() { app.netplay.game_info = data; + app.netplay.rom_contents = app.netplay.game_info.3.clone(); app.netplay.create_rom_label = app.netplay.game_info.2.clone(); } else { app.netplay.create_rom_label = data.2; @@ -278,6 +297,7 @@ pub fn netplay_create(app: &mut GopherEguiApp, ctx: &egui::Context) { } else if app.netplay.game_info.0.is_empty() { app.netplay.error = "ROM not loaded".to_string(); } else { + app.netplay.rom_contents = app.netplay.game_info.3.clone(); let now_utc = chrono::Utc::now().timestamp_millis().to_string(); let hasher = Sha256::new().chain_update(&now_utc).chain_update(EMU_NAME); let mut game_name = app.netplay.game_info.1.trim().to_string(); @@ -315,6 +335,7 @@ pub fn netplay_create(app: &mut GopherEguiApp, ctx: &egui::Context) { .unwrap(); match socket.get_mut() { tungstenite::stream::MaybeTlsStream::Plain(stream) => { + app.netplay.peer_addr = Some(stream.peer_addr().unwrap()); stream.set_nonblocking(true) } _ => unimplemented!(), @@ -346,7 +367,10 @@ fn get_sessions(app: &mut GopherEguiApp, ctx: &egui::Context) { let (mut sock, _response) = tungstenite::connect(&app.netplay.server.1).expect("Can't connect"); match sock.get_mut() { - tungstenite::stream::MaybeTlsStream::Plain(stream) => stream.set_nonblocking(true), + tungstenite::stream::MaybeTlsStream::Plain(stream) => { + app.netplay.peer_addr = Some(stream.peer_addr().unwrap()); + stream.set_nonblocking(true) + } _ => unimplemented!(), } .expect("could not set socket to non-blocking"); @@ -555,13 +579,16 @@ pub fn netplay_join(app: &mut GopherEguiApp, ctx: &egui::Context) { std::str::from_utf8(&rom_contents[0x20..0x20 + 0x14]) .unwrap() .to_string(); - let _ = tx.send((hash, game_name, file.file_name())).await; + let _ = tx + .send((hash, game_name, file.file_name(), rom_contents)) + .await; } else { let _ = tx .send(( "".to_string(), "".to_string(), "Invalid ROM".to_string(), + vec![], )) .await; } @@ -571,6 +598,7 @@ pub fn netplay_join(app: &mut GopherEguiApp, ctx: &egui::Context) { "".to_string(), "".to_string(), "No ROM selected".to_string(), + vec![], )) .await; } @@ -712,6 +740,15 @@ pub fn netplay_wait(app: &mut GopherEguiApp, ctx: &egui::Context) { app.netplay.chat_log.push_str(&message.message.unwrap()); app.netplay.chat_log.push('\n'); } else if message.message_type == "reply_begin_game" { + let mut player = 0; + for (i, name) in app.netplay.player_names.iter().enumerate() { + if *name == app.netplay.player_name { + player = i; + break; + } + } + app.netplay.player_number = player as u8; + gui::open_rom(app, ctx); app.netplay = Default::default(); return; }