diff --git a/src/ui/gui/netplay.rs b/src/ui/gui/netplay.rs index f957dcd..bb00209 100644 --- a/src/ui/gui/netplay.rs +++ b/src/ui/gui/netplay.rs @@ -15,13 +15,17 @@ pub struct Netplay { pub password: String, pub player_name: String, pub error: String, - pub rom_label: String, + pub create_rom_label: String, + pub join_rom_label: String, pub send_chat: bool, + pub have_sessions: Option<(String, String)>, pub begin_game: bool, pub chat_log: String, pub chat_message: String, + pub selected_session: Option, pub pending_begin: bool, pub motd: String, + pub sessions: Vec, pub player_names: [String; 4], pub server: (String, String), pub socket_waiting: bool, @@ -37,7 +41,7 @@ pub struct Netplay { pub broadcast_timer: Option, } -#[derive(serde::Serialize, serde::Deserialize)] +#[derive(serde::Serialize, serde::Deserialize, PartialEq, Default, Clone)] pub struct NetplayRoom { room_name: Option, password: Option, @@ -63,6 +67,90 @@ pub struct NetplayMessage { player_names: Option<[String; 4]>, #[serde(rename = "authTime")] auth_time: Option, + rooms: Option>, +} + +fn get_servers(app: &mut GopherEguiApp, ctx: &egui::Context) { + if app.netplay.servers.is_empty() { + if app.netplay.broadcast_socket.is_none() { + app.netplay.broadcast_socket = Some( + std::net::UdpSocket::bind((std::net::Ipv4Addr::UNSPECIFIED, 0)) + .expect("couldn't bind to address"), + ); + let socket = app.netplay.broadcast_socket.as_ref().unwrap(); + socket + .set_broadcast(true) + .expect("set_broadcast call failed"); + socket + .set_nonblocking(true) + .expect("could not set up socket"); + let data: [u8; 1] = [1]; + socket + .send_to(&data, (std::net::Ipv4Addr::BROADCAST, 45000)) + .expect("couldn't send data"); + app.netplay.broadcast_timer = + Some(std::time::Instant::now() + std::time::Duration::from_secs(5)); + ctx.request_repaint(); + } + if app.netplay.server_receiver.is_none() { + let (tx, rx) = tokio::sync::mpsc::channel(1); + app.netplay.server_receiver = Some(rx); + let gui_ctx = ctx.clone(); + tokio::spawn(async move { + if let Ok(response) = + reqwest::get("https://m64p.s3.amazonaws.com/servers.json").await + { + if let Ok(servers) = response + .json::>() + .await + { + let _ = tx.send(servers).await; + gui_ctx.request_repaint(); + } + } + }); + } + } + if app.netplay.broadcast_timer.is_some() + && std::time::Instant::now() > app.netplay.broadcast_timer.unwrap() + { + app.netplay.broadcast_timer = None; + } + if app.netplay.broadcast_socket.is_some() && app.netplay.broadcast_timer.is_some() { + let mut buffer = [0; 1024]; + let result = app + .netplay + .broadcast_socket + .as_ref() + .unwrap() + .recv_from(&mut buffer); + if result.is_ok() { + let (amt, _src) = result.unwrap(); + let data: std::collections::HashMap = + serde_json::from_slice(&buffer[..amt]).unwrap(); + for server in data.iter() { + let (server_name, server_ip) = server; + app.netplay + .servers + .insert(server_name.to_string(), server_ip.to_string()); + app.netplay.server = (server.0.clone(), server.1.clone()); + } + app.netplay.broadcast_socket = None; + } + ctx.request_repaint(); + } + if app.netplay.server_receiver.is_some() { + let result = app.netplay.server_receiver.as_mut().unwrap().try_recv(); + if result.is_ok() { + app.netplay.servers.extend(result.unwrap()); + app.netplay.server_receiver = None; + if app.netplay.server.0.is_empty() { + let first_server = app.netplay.servers.iter().next().unwrap(); + app.netplay.server = (first_server.0.clone(), first_server.1.clone()); + } + } + ctx.request_repaint(); + } } pub fn netplay_create(app: &mut GopherEguiApp, ctx: &egui::Context) { @@ -86,15 +174,15 @@ pub fn netplay_create(app: &mut GopherEguiApp, ctx: &egui::Context) { ui.end_row(); ui.label("ROM"); - if app.netplay.rom_label.is_empty() { - app.netplay.rom_label = "Open ROM".to_string(); + if app.netplay.create_rom_label.is_empty() { + app.netplay.create_rom_label = "Open ROM".to_string(); } - if ui.button(&app.netplay.rom_label).clicked() { + if ui.button(&app.netplay.create_rom_label).clicked() { let task = rfd::AsyncFileDialog::new().pick_file(); let (tx, rx) = tokio::sync::mpsc::channel(1); app.netplay.game_info_receiver = Some(rx); let gui_ctx = ctx.clone(); - app.netplay.rom_label = "Inspecting ROM".to_string(); + app.netplay.create_rom_label = "Inspecting ROM".to_string(); tokio::spawn(async move { let file = task.await; @@ -131,86 +219,8 @@ pub fn netplay_create(app: &mut GopherEguiApp, ctx: &egui::Context) { ui.label("Server:"); - if app.netplay.servers.is_empty() { - if app.netplay.broadcast_socket.is_none() { - app.netplay.broadcast_socket = Some( - std::net::UdpSocket::bind((std::net::Ipv4Addr::UNSPECIFIED, 0)) - .expect("couldn't bind to address"), - ); - let socket = app.netplay.broadcast_socket.as_ref().unwrap(); - socket - .set_broadcast(true) - .expect("set_broadcast call failed"); - socket - .set_nonblocking(true) - .expect("could not set up socket"); - let data: [u8; 1] = [1]; - socket - .send_to(&data, (std::net::Ipv4Addr::BROADCAST, 45000)) - .expect("couldn't send data"); - app.netplay.broadcast_timer = - Some(std::time::Instant::now() + std::time::Duration::from_secs(5)); - ctx.request_repaint(); - } - if app.netplay.server_receiver.is_none() { - let (tx, rx) = tokio::sync::mpsc::channel(1); - app.netplay.server_receiver = Some(rx); - let gui_ctx = ctx.clone(); - tokio::spawn(async move { - if let Ok(response) = - reqwest::get("https://m64p.s3.amazonaws.com/servers.json").await - { - if let Ok(servers) = response - .json::>() - .await - { - let _ = tx.send(servers).await; - gui_ctx.request_repaint(); - } - } - }); - } - } - if app.netplay.broadcast_timer.is_some() - && std::time::Instant::now() > app.netplay.broadcast_timer.unwrap() - { - app.netplay.broadcast_timer = None; - } - if app.netplay.broadcast_socket.is_some() && app.netplay.broadcast_timer.is_some() { - let mut buffer = [0; 1024]; - let result = app - .netplay - .broadcast_socket - .as_ref() - .unwrap() - .recv_from(&mut buffer); - if result.is_ok() { - let (amt, _src) = result.unwrap(); - let data: std::collections::HashMap = - serde_json::from_slice(&buffer[..amt]).unwrap(); - for server in data.iter() { - let (server_name, server_ip) = server; - app.netplay - .servers - .insert(server_name.to_string(), server_ip.to_string()); - app.netplay.server = (server.0.clone(), server.1.clone()); - } - app.netplay.broadcast_socket = None; - } - ctx.request_repaint(); - } - if app.netplay.server_receiver.is_some() { - let result = app.netplay.server_receiver.as_mut().unwrap().try_recv(); - if result.is_ok() { - app.netplay.servers.extend(result.unwrap()); - app.netplay.server_receiver = None; - if app.netplay.server.0.is_empty() { - let first_server = app.netplay.servers.iter().next().unwrap(); - app.netplay.server = (first_server.0.clone(), first_server.1.clone()); - } - } - ctx.request_repaint(); - } + get_servers(app, ctx); + if app.netplay.game_info_receiver.is_some() { let result = app.netplay.game_info_receiver.as_mut().unwrap().try_recv(); if result.is_ok() { @@ -218,9 +228,9 @@ 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_label = app.netplay.game_info.2.clone(); + app.netplay.create_rom_label = app.netplay.game_info.2.clone(); } else { - app.netplay.rom_label = data.2; + app.netplay.create_rom_label = data.2; } } ctx.request_repaint(); @@ -273,7 +283,7 @@ pub fn netplay_create(app: &mut GopherEguiApp, ctx: &egui::Context) { let mut game_name = app.netplay.game_info.1.trim().to_string(); if game_name.is_empty() { // If the ROM doesn't report a name, use the filename - game_name = app.netplay.rom_label.clone(); + game_name = app.netplay.create_rom_label.clone(); } let netplay_message = NetplayMessage { message_type: "request_create_room".to_string(), @@ -283,6 +293,7 @@ pub fn netplay_create(app: &mut GopherEguiApp, ctx: &egui::Context) { emulator: Some(EMU_NAME.to_string()), accept: None, message: None, + rooms: None, auth_time: Some(now_utc), player_names: None, auth: Some(format!("{:x}", hasher.finalize())), @@ -323,14 +334,250 @@ pub fn netplay_create(app: &mut GopherEguiApp, ctx: &egui::Context) { }); } +fn get_sessions(app: &mut GopherEguiApp, ctx: &egui::Context) { + if app.netplay.have_sessions.is_some() + && app.netplay.server != app.netplay.have_sessions.as_ref().unwrap().clone() + { + // User has changed the server + app.netplay.have_sessions = None; + app.netplay.socket = None; + } + if app.netplay.socket.is_none() { + 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), + _ => unimplemented!(), + } + .expect("could not set socket to non-blocking"); + app.netplay.socket = Some(sock); + } + let socket = app.netplay.socket.as_mut().unwrap(); + if app.netplay.have_sessions.is_none() { + let now_utc = chrono::Utc::now().timestamp_millis().to_string(); + let hasher = Sha256::new().chain_update(&now_utc).chain_update(EMU_NAME); + let request_rooms = NetplayMessage { + message_type: "request_get_rooms".to_string(), + player_name: None, + client_sha: None, + netplay_version: Some(NETPLAY_VERSION), + player_names: None, + emulator: Some(EMU_NAME.to_string()), + accept: None, + rooms: None, + message: None, + auth_time: Some(now_utc), + auth: Some(format!("{:x}", hasher.finalize())), + room: None, + }; + + socket + .send(tungstenite::Message::Binary(tungstenite::Bytes::from( + serde_json::to_vec(&request_rooms).unwrap(), + ))) + .unwrap(); + app.netplay.have_sessions = Some(app.netplay.server.clone()); + app.netplay.socket_waiting = true; + ctx.request_repaint(); + } +} + pub fn netplay_join(app: &mut GopherEguiApp, ctx: &egui::Context) { + if app.netplay.socket_waiting { + let socket = app.netplay.socket.as_mut().unwrap(); + let data = socket.read(); + if data.is_ok() { + let message: NetplayMessage = + serde_json::from_slice(&data.unwrap().into_data()).unwrap(); + if message.accept.unwrap() == 0 { + if message.message_type == "reply_get_rooms" { + if message.rooms.is_some() { + app.netplay.sessions = message.rooms.unwrap(); + } + } else if message.message_type == "reply_join_room" { + app.netplay.join = false; + app.netplay.wait = true; + app.netplay.waiting_session = Some(message.room.unwrap()); + } + app.netplay.socket_waiting = false; + } else { + app.netplay.error = message.message.unwrap(); + app.netplay.join_rom_label = "Join Session (Open ROM)".to_string(); + } + } + ctx.request_repaint(); + } + if app.netplay.game_info_receiver.is_some() { + let result = app.netplay.game_info_receiver.as_mut().unwrap().try_recv(); + if result.is_ok() { + app.netplay.game_info_receiver = None; + let data = result.unwrap(); + if !data.0.is_empty() { + app.netplay.game_info = data; + + let netplay_message = NetplayMessage { + message_type: "request_join_room".to_string(), + player_name: Some(app.netplay.player_name.clone()), + client_sha: Some(env!("CARGO_PKG_VERSION").to_string()), + netplay_version: None, + emulator: None, + accept: None, + message: None, + rooms: None, + auth_time: None, + player_names: None, + auth: None, + room: Some(NetplayRoom { + room_name: None, + password: Some(app.netplay.password.clone()), + game_name: None, + md5: Some(app.netplay.game_info.0.clone()), + protected: None, + port: app.netplay.selected_session.as_ref().unwrap().port, + }), + }; + let socket = app.netplay.socket.as_mut().unwrap(); + socket + .send(tungstenite::Message::Binary(tungstenite::Bytes::from( + serde_json::to_vec(&netplay_message).unwrap(), + ))) + .unwrap(); + + app.netplay.socket_waiting = true; + } else { + app.netplay.error = data.2; + app.netplay.join_rom_label = "Join Session (Open ROM)".to_string(); + } + } + ctx.request_repaint(); + } egui::Window::new("Join Netplay Session").show(ctx, |ui| { - egui::Grid::new("netplay_join_grid").show(ui, |_ui| {}); ui.horizontal(|ui| { + let mut size = ui.spacing().interact_size; + size.x = 100.0; + ui.add_sized( + size, + egui::TextEdit::singleline(&mut app.netplay.player_name).hint_text("Player name"), + ); + + get_servers(app, ctx); + + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + if ui.button("Refresh").clicked() { + app.netplay.socket = None; + app.netplay.have_sessions = None; + app.netplay.selected_session = None; + ctx.request_repaint(); + } + egui::ComboBox::from_id_salt("server-combobox") + .selected_text(app.netplay.server.0.to_string()) + .show_ui(ui, |ui| { + for server in app.netplay.servers.iter() { + ui.selectable_value( + &mut app.netplay.server, + (server.0.clone(), server.1.clone()), + server.0, + ); + } + }); + }); + }); + if !app.netplay.server.0.is_empty() { + get_sessions(app, ctx); + } + ui.add_space(16.0); + if app.netplay.sessions.is_empty() { + ui.label("No sessions available"); + } else { + egui::Grid::new("netplay_join_grid").show(ui, |ui| { + ui.label(egui::RichText::new("Session Name (click to select)").underline()); + ui.label(egui::RichText::new("Game Name").underline()); + ui.label(egui::RichText::new("Password Protected").underline()); + ui.end_row(); + + for room in app.netplay.sessions.iter() { + ui.selectable_value( + &mut app.netplay.selected_session, + Some(room.clone()), + room.room_name.as_ref().unwrap(), + ); + ui.label(room.game_name.as_ref().unwrap()); + ui.label(room.protected.unwrap_or(false).to_string()); + ui.end_row(); + } + }); + } + ui.add_space(16.0); + ui.horizontal(|ui| { + let mut size = ui.spacing().interact_size; + size.x = 130.0; + ui.add_sized( + size, + egui::TextEdit::singleline(&mut app.netplay.password) + .hint_text("Password (if required)"), + ); ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { if ui.button("Close").clicked() { app.netplay = Default::default(); }; + if app.netplay.join_rom_label.is_empty() { + app.netplay.join_rom_label = "Join Session (Open ROM)".to_string(); + } + if ui.button(&app.netplay.join_rom_label).clicked() { + if app.netplay.player_name.is_empty() { + app.netplay.error = "Player Name cannot be empty".to_string(); + } else if app.netplay.selected_session.is_none() { + app.netplay.error = "No session selected".to_string(); + } else if app + .netplay + .selected_session + .as_ref() + .unwrap() + .protected + .unwrap() + && app.netplay.password.is_empty() + { + app.netplay.error = "Session requires a password".to_string(); + } else { + let task = rfd::AsyncFileDialog::new().pick_file(); + let (tx, rx) = tokio::sync::mpsc::channel(1); + app.netplay.game_info_receiver = Some(rx); + let gui_ctx = ctx.clone(); + app.netplay.join_rom_label = "Inspecting ROM".to_string(); + tokio::spawn(async move { + let file = task.await; + + if let Some(file) = file { + let rom_contents = device::get_rom_contents(file.path()); + if !rom_contents.is_empty() { + let hash = device::cart_rom::calculate_hash(&rom_contents); + 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; + } else { + let _ = tx + .send(( + "".to_string(), + "".to_string(), + "Invalid ROM".to_string(), + )) + .await; + } + } else { + let _ = tx + .send(( + "".to_string(), + "".to_string(), + "No ROM selected".to_string(), + )) + .await; + } + gui_ctx.request_repaint(); + }); + } + }; }); }); }); @@ -344,6 +591,7 @@ pub fn netplay_wait(app: &mut GopherEguiApp, ctx: &egui::Context) { netplay_version: None, emulator: None, accept: None, + rooms: None, player_names: None, message: None, auth_time: None, @@ -357,6 +605,7 @@ pub fn netplay_wait(app: &mut GopherEguiApp, ctx: &egui::Context) { netplay_version: None, player_names: None, emulator: None, + rooms: None, accept: None, message: None, auth_time: None, @@ -392,6 +641,7 @@ pub fn netplay_wait(app: &mut GopherEguiApp, ctx: &egui::Context) { client_sha: None, netplay_version: None, player_names: None, + rooms: None, emulator: None, accept: None, message: None, @@ -421,6 +671,7 @@ pub fn netplay_wait(app: &mut GopherEguiApp, ctx: &egui::Context) { client_sha: None, netplay_version: None, player_names: None, + rooms: None, emulator: None, accept: None, message: Some(app.netplay.chat_message.clone()),