#[cfg(not(target_os = "android"))] use crate::device; use crate::ui; use crate::ui::gui::{ AppWindow, CustomNetplayServer, DispatcherDialog, ErrorDialog, NetplayCreate, NetplayDevice, NetplayJoin, NetplayWait, run_rom, save_settings, }; use futures::{SinkExt, StreamExt}; use sha2::{Digest, Sha256}; use slint::{ComponentHandle, Model}; use tokio_tungstenite::tungstenite::Bytes; use tokio_tungstenite::tungstenite::protocol::Message; const NETPLAY_VERSION: i32 = 17; const EMU_NAME: &str = "gopher64"; #[derive(serde::Serialize, serde::Deserialize, Clone, Debug)] pub struct NetplayRoom { room_name: Option, password: Option, protected: Option, #[serde(rename = "MD5")] md5: Option, game_name: Option, pub port: Option, features: Option>, buffer_target: Option, } #[derive(serde::Serialize, serde::Deserialize, Clone, Debug)] pub struct NetplayMessage { #[serde(rename = "type")] message_type: String, player_name: Option, client_sha: Option, netplay_version: Option, emulator: Option, room: Option, accept: Option, message: Option, auth: Option, player_names: Option<[String; 4]>, #[serde(rename = "authTime")] auth_time: Option, rooms: Option>, } trait NetplayPages { fn set_server_names(&self, names: slint::ModelRc); fn set_server_urls(&self, urls: slint::ModelRc); #[cfg(not(target_os = "android"))] fn set_game_name(&self, game_name: slint::SharedString); #[cfg(not(target_os = "android"))] fn set_game_hash(&self, game_hash: slint::SharedString); #[cfg(not(target_os = "android"))] fn set_game_cheats(&self, game_cheats: slint::SharedString); #[cfg(not(target_os = "android"))] fn set_rom_path(&self, rom_path: slint::SharedString); fn set_peer_addr(&self, peer_addr: slint::SharedString); fn refresh_sessions(&self) { // Default implementation does nothing } } impl NetplayPages for NetplayCreate { fn set_server_names(&self, names: slint::ModelRc) { self.set_server_names(names); } fn set_server_urls(&self, urls: slint::ModelRc) { self.set_server_urls(urls); } #[cfg(not(target_os = "android"))] fn set_game_name(&self, game_name: slint::SharedString) { self.set_game_name(game_name); } #[cfg(not(target_os = "android"))] fn set_game_hash(&self, game_hash: slint::SharedString) { self.set_game_hash(game_hash); } #[cfg(not(target_os = "android"))] fn set_game_cheats(&self, game_cheats: slint::SharedString) { self.set_game_cheats(game_cheats); } #[cfg(not(target_os = "android"))] fn set_rom_path(&self, rom_path: slint::SharedString) { self.set_rom_path(rom_path); } fn set_peer_addr(&self, peer_addr: slint::SharedString) { self.set_peer_addr(peer_addr); } } impl NetplayPages for NetplayJoin { fn set_server_names(&self, names: slint::ModelRc) { self.set_server_names(names); } fn set_server_urls(&self, urls: slint::ModelRc) { self.set_server_urls(urls); } fn refresh_sessions(&self) { self.invoke_refresh_session(); } #[cfg(not(target_os = "android"))] fn set_game_name(&self, game_name: slint::SharedString) { self.set_game_name(game_name); } #[cfg(not(target_os = "android"))] fn set_game_hash(&self, game_hash: slint::SharedString) { self.set_game_hash(game_hash); } #[cfg(not(target_os = "android"))] fn set_game_cheats(&self, game_cheats: slint::SharedString) { self.set_game_cheats(game_cheats); } #[cfg(not(target_os = "android"))] fn set_rom_path(&self, rom_path: slint::SharedString) { self.set_rom_path(rom_path); } fn set_peer_addr(&self, peer_addr: slint::SharedString) { self.set_peer_addr(peer_addr); } } fn populate_server_names(weak: slint::Weak) { let task = ui::WEB_CLIENT .get("https://dispatch.gopher64.com/getRegions") .header("netplay-id", env!("NETPLAY_ID")) .send(); tokio::spawn(async move { let mut local_servers: Vec<(String, String)> = vec![]; if let Ok(broadcast_sock) = tokio::net::UdpSocket::bind((std::net::Ipv4Addr::UNSPECIFIED, 0)).await && broadcast_sock.set_broadcast(true).is_ok() { let data: [u8; 1] = [1]; if let Err(e) = broadcast_sock .send_to(&data, (std::net::Ipv4Addr::BROADCAST, 45000)) .await { eprintln!("Error sending broadcast: {}", e); } let mut buffer = [0; 1024]; if let Ok(Ok(result)) = tokio::time::timeout( std::time::Duration::from_millis(200), broadcast_sock.recv(&mut buffer), ) .await && let Ok(data) = serde_json::from_slice::>( &buffer[..result], ) { for server in data.iter() { local_servers.push((server.0.into(), server.1.into())); } } } else { eprintln!("Error creating netplay broadcast socket"); } let response = task.await; let public_servers = if let Ok(response) = response && let Ok(servers) = response.json::>().await { servers } else { vec![] }; weak.upgrade_in_event_loop(move |handle| { let server_names: slint::VecModel = slint::VecModel::default(); let server_urls: slint::VecModel = slint::VecModel::default(); for local_server in local_servers { server_names.push(local_server.0.into()); server_urls.push(local_server.1.into()); } for server in public_servers { server_names.push(server.clone().into()); server_urls.push(format!("dispatcher:{server}").into()); } server_names.push("Custom".into()); handle.refresh_sessions(); handle.set_server_names(slint::ModelRc::from(std::rc::Rc::new(server_names))); handle.set_server_urls(slint::ModelRc::from(std::rc::Rc::new(server_urls))); }) .unwrap(); }); } #[cfg(not(target_os = "android"))] fn select_rom( weak: slint::Weak, rom_dir: slint::SharedString, ) { let select_rom = if !rom_dir.is_empty() && std::fs::exists(&rom_dir).unwrap_or(false) { rfd::AsyncFileDialog::new().set_directory(rom_dir) } else { rfd::AsyncFileDialog::new() } .set_title("Select ROM") .add_filter("ROM files", &ui::gui::N64_EXTENSIONS) .pick_file(); tokio::spawn(async move { if let Some(file) = select_rom.await { if let Some(rom_contents) = device::get_rom_contents(file.path()) { let hash = device::cart::rom::calculate_hash(&rom_contents); let mut game_name = ui::storage::get_game_name(&rom_contents); let game_crc = ui::storage::get_game_crc(&rom_contents); let cheats = ui::config::Cheats::new(); let mut parsed_cheats = String::new(); if let Some(game_cheats) = cheats.cheats.get(&game_crc) && !game_cheats.is_empty() { parsed_cheats = serde_json::to_string(game_cheats).unwrap(); } if game_name.is_empty() { game_name = file .path() .file_name() .unwrap() .to_string_lossy() .to_string(); } weak.upgrade_in_event_loop(move |handle| { handle.set_game_name(game_name.into()); handle.set_game_hash(hash.into()); handle.set_game_cheats(parsed_cheats.into()); handle.set_rom_path(file.path().to_str().unwrap().into()); }) .unwrap(); } else { weak.upgrade_in_event_loop(move |handle| { let message_dialog = ErrorDialog::new().unwrap(); let weak_dialog = message_dialog.as_weak(); message_dialog.on_close_clicked(move || { weak_dialog.unwrap().hide().unwrap(); }); message_dialog.set_text("Could not read ROM".into()); message_dialog.show().unwrap(); handle.set_game_name(String::new().into()); handle.set_game_hash(String::new().into()); handle.set_game_cheats(String::new().into()); handle.set_rom_path(String::new().into()); }) .unwrap(); } } }); } fn show_custom_url_dialog(weak: slint::Weak, server_url: slint::SharedString) { let url_dialog = CustomNetplayServer::new().unwrap(); url_dialog.set_custom_server_url(server_url); let weak_dialog = url_dialog.as_weak(); url_dialog.on_ok_clicked(move |server_url| { weak.upgrade_in_event_loop(move |handle| { handle.set_custom_server_url(server_url.clone()); }) .unwrap(); weak_dialog.unwrap().hide().unwrap(); }); url_dialog.show().unwrap(); } pub fn setup_create_window( create_window: &NetplayCreate, game_settings: ui::GameSettings, rom_dir: slint::SharedString, weak_app: slint::Weak, ) { let (netplay_read_sender, netplay_read_receiver): ( tokio::sync::broadcast::Sender, tokio::sync::broadcast::Receiver, ) = tokio::sync::broadcast::channel(5); let (netplay_write_sender, netplay_write_receiver): ( tokio::sync::broadcast::Sender>, tokio::sync::broadcast::Receiver>, ) = tokio::sync::broadcast::channel(5); create_window.set_rom_dir(rom_dir); populate_server_names(create_window.as_weak()); let weak = create_window.as_weak(); create_window.on_get_custom_url(move || { let weak2 = weak.clone(); weak.upgrade_in_event_loop(move |handle| { show_custom_url_dialog(weak2, handle.get_custom_server_url()); }) .unwrap(); }); #[cfg(not(target_os = "android"))] { let weak = create_window.as_weak(); create_window.on_select_rom(move |rom_dir| { select_rom(weak.clone(), rom_dir); }); } let weak = create_window.as_weak(); create_window.on_create_session( move |server_url, session_name, player_name, game_name, game_hash, game_cheats, password| { let _ = netplay_write_sender.send(None); // close current websocket if any if server_url.starts_with("dispatcher:") { let message_dialog = DispatcherDialog::new().unwrap(); let weak_dialog = message_dialog.as_weak(); message_dialog.show().unwrap(); let task = ui::WEB_CLIENT .get("https://dispatch.gopher64.com/createServer") .query(&[("region", server_url.strip_prefix("dispatcher:").unwrap())]) .header("netplay-id", env!("NETPLAY_ID")) .send(); let netplay_read_sender = netplay_read_sender.clone(); let netplay_write_receiver = netplay_write_receiver.resubscribe(); let netplay_write_sender = netplay_write_sender.clone(); let netplay_read_receiver = netplay_read_receiver.resubscribe(); let game_settings = game_settings.clone(); let weak = weak.clone(); let weak_app = weak_app.clone(); tokio::spawn(async move { let response = task.await; weak_dialog .upgrade_in_event_loop(move |handle| { handle.hide().unwrap(); }) .unwrap(); if let Ok(response) = response && let Ok(server) = response .json::>() .await { let server_url = server.values().next().unwrap(); manage_websocket( server_url.to_string(), netplay_read_sender, netplay_write_receiver, weak.clone(), ); create_session( netplay_write_sender, netplay_read_receiver, session_name.to_string(), player_name.to_string(), game_name.to_string(), game_hash.to_string(), game_cheats.to_string(), password.to_string(), game_settings, weak_app, weak.clone(), ); } else { weak.upgrade_in_event_loop(|handle| { handle.set_pending_session(false); show_netplay_error("Server could not be created".to_string()); }) .unwrap(); } }); } else { manage_websocket( server_url.to_string(), netplay_read_sender.clone(), netplay_write_receiver.resubscribe(), weak.clone(), ); create_session( netplay_write_sender.clone(), netplay_read_receiver.resubscribe(), session_name.to_string(), player_name.to_string(), game_name.to_string(), game_hash.to_string(), game_cheats.to_string(), password.to_string(), game_settings.clone(), weak_app.clone(), weak.clone(), ); } }, ); create_window.show().unwrap(); } fn manage_websocket( server_url: String, netplay_read_sender: tokio::sync::broadcast::Sender, mut netplay_write_receiver: tokio::sync::broadcast::Receiver>, weak: slint::Weak, ) { tokio::spawn(async move { if let Ok(Ok((socket, _response))) = tokio::time::timeout( std::time::Duration::from_secs(2), tokio_tungstenite::connect_async(server_url.clone()), ) .await { match socket.get_ref() { tokio_tungstenite::MaybeTlsStream::Plain(stream) => { let addr = stream.peer_addr().unwrap().to_string(); weak.upgrade_in_event_loop(move |handle| { handle.set_peer_addr(addr.into()); }) .unwrap(); } _ => unimplemented!(), } let (mut write, mut read) = socket.split(); tokio::spawn(async move { while let Some(Ok(response)) = read.next().await { if let Ok(message) = serde_json::from_slice(&response.into_data()) { let _ = netplay_read_sender.send(message); } } }); tokio::spawn(async move { loop { match netplay_write_receiver.recv().await { Ok(Some(response)) => { write .send(Message::Binary(Bytes::from( serde_json::to_vec(&response).unwrap(), ))) .await .unwrap(); } Ok(None) => { break; } Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => { panic!("netplay_write_receiver lagged"); } Err(tokio::sync::broadcast::error::RecvError::Closed) => { break; // exit the loop if the receiver is closed } } } write.close().await.unwrap(); }); } }); } fn show_netplay_error(message: String) { let message_dialog = ErrorDialog::new().unwrap(); let weak_dialog = message_dialog.as_weak(); message_dialog.on_close_clicked(move || { weak_dialog.unwrap().hide().unwrap(); }); message_dialog.set_text(message.into()); message_dialog.show().unwrap(); } fn update_sessions(weak: slint::Weak) { let task = ui::WEB_CLIENT .get("https://dispatch.gopher64.com/getServers") .header("netplay-id", env!("NETPLAY_ID")) .send(); tokio::spawn(async move { let mut dispatcher_servers = std::collections::HashMap::new(); let response = task.await; if let Ok(response) = response && let Ok(servers) = response .json::>() .await { dispatcher_servers = servers; } let weak2 = weak.clone(); weak.upgrade_in_event_loop(move |handle| { let mut servers: std::collections::HashMap = std::collections::HashMap::new(); let server_names = handle.get_server_names(); let server_urls = handle.get_server_urls(); for (i, server_name) in server_names.iter().enumerate() { if server_name == "Custom" { let custom_server_url = handle.get_custom_server_url(); if !custom_server_url.is_empty() { servers.insert( "Custom".to_string(), "ws://".to_string() + &custom_server_url, ); } } else if let Some(url) = server_urls.row_data(i) && url.starts_with("dispatcher:") { continue; } else { servers.insert( server_name.to_string(), server_urls.row_data(i).unwrap().to_string(), ); } } servers.extend(dispatcher_servers); tokio::spawn(async move { let mut sessions = vec![]; let mut room_urls = vec![]; let mut room_ports = vec![]; for (server_name, server_url) in servers.iter() { if let Ok(Ok((socket, _response))) = tokio::time::timeout( std::time::Duration::from_secs(2), tokio_tungstenite::connect_async(server_url.clone()), ) .await { let (mut write, mut read) = socket.split(); let now_utc = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_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( hasher .finalize() .iter() .map(|b| format!("{:02x}", b)) .collect(), ), room: None, }; write .send(Message::Binary(Bytes::from( serde_json::to_vec(&request_rooms).unwrap(), ))) .await .unwrap(); if let Some(Ok(response)) = read.next().await && let Ok(message) = serde_json::from_slice::(&response.into_data()) && message.message_type == "reply_get_rooms" && message.accept.unwrap() == 0 && let Some(rooms) = message.rooms { for room in rooms { let mut session = vec![]; room_urls.push(server_url.into()); room_ports.push(room.port.unwrap()); session.push(slint::StandardListViewItem::from( slint::SharedString::from(server_name), )); session.push(slint::StandardListViewItem::from( slint::SharedString::from(room.room_name.unwrap()), )); session.push(slint::StandardListViewItem::from( slint::SharedString::from(room.game_name.unwrap()), )); session.push(slint::StandardListViewItem::from( slint::SharedString::from(if room.protected.unwrap() { "True" } else { "False" }), )); session.push(slint::StandardListViewItem::from( slint::SharedString::from( if room.features.unwrap_or_default().contains_key("cheats") { "True" } else { "False" }, ), )); sessions.push(session); } } } } weak2 .upgrade_in_event_loop(move |handle| { let sessions_vec = slint::VecModel::default(); for session in sessions.iter() { sessions_vec.push(slint::ModelRc::from(std::rc::Rc::new( slint::VecModel::from(session.to_vec()), ))); } handle.set_sessions(slint::ModelRc::from(std::rc::Rc::new(sessions_vec))); handle.set_room_urls(slint::ModelRc::from(std::rc::Rc::new( slint::VecModel::from(room_urls.to_vec()), ))); handle.set_room_ports(slint::ModelRc::from(std::rc::Rc::new( slint::VecModel::from(room_ports.to_vec()), ))); handle.set_current_session(-1); handle.set_pending_refresh(false); }) .unwrap(); }); }) .unwrap(); }); } #[allow(clippy::too_many_arguments)] fn create_session( netplay_write_sender: tokio::sync::broadcast::Sender>, mut netplay_read_receiver: tokio::sync::broadcast::Receiver, session_name: String, player_name: String, game_name: String, game_hash: String, game_cheats: String, password: String, game_settings: ui::GameSettings, weak_app: slint::Weak, weak: slint::Weak, ) { tokio::spawn(async move { let now_utc = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_millis() .to_string(); let hasher = Sha256::new().chain_update(&now_utc).chain_update(EMU_NAME); let mut features = std::collections::HashMap::new(); if !game_cheats.is_empty() { features.insert("cheats".to_string(), game_cheats); } features.insert("overclock".to_string(), game_settings.overclock.to_string()); features.insert( "disable_expansion_pak".to_string(), game_settings.disable_expansion_pak.to_string(), ); let create_room = NetplayMessage { message_type: "request_create_room".to_string(), player_name: Some(player_name), client_sha: Some(env!("GIT_DESCRIBE").to_string()), netplay_version: Some(NETPLAY_VERSION), emulator: Some(EMU_NAME.to_string()), accept: None, message: None, rooms: None, auth_time: Some(now_utc), player_names: None, auth: Some( hasher .finalize() .iter() .map(|b| format!("{:02x}", b)) .collect(), ), room: Some(NetplayRoom { room_name: Some(session_name), password: Some(password), game_name: Some(game_name), md5: Some(game_hash), protected: None, port: None, features: Some(features), buffer_target: None, }), }; netplay_write_sender.send(Some(create_room)).unwrap(); match tokio::time::timeout( std::time::Duration::from_secs(2), netplay_read_receiver.recv(), ) .await { Ok(Ok(message)) => { if message.message_type == "reply_create_room" && message.accept.unwrap() == 0 { weak.upgrade_in_event_loop(move |handle| { let session = message.room.as_ref().unwrap(); let features_default = "false".to_string(); let cheats_default = "{}".to_string(); let overclock = session .features .as_ref() .unwrap() .get("overclock") .unwrap_or(&features_default); let disable_expansion_pak = session .features .as_ref() .unwrap() .get("disable_expansion_pak") .unwrap_or(&features_default); let cheats = session .features .as_ref() .unwrap() .get("cheats") .unwrap_or(&cheats_default); setup_wait_window( netplay_write_sender, netplay_read_receiver, session.room_name.as_ref().unwrap().into(), session.game_name.as_ref().unwrap().into(), handle.get_rom_path(), message.player_name.as_ref().unwrap().into(), session.port.unwrap(), ui::GameSettings { overclock: overclock.parse().unwrap(), disable_expansion_pak: disable_expansion_pak.parse().unwrap(), cheats: serde_json::from_str(cheats).unwrap(), load_savestate_slot: None, }, handle.get_peer_addr(), weak_app, ); handle.hide().unwrap(); }) .unwrap(); } else { weak.upgrade_in_event_loop(move |handle| { handle.set_pending_session(false); if let Some(message) = message.message { show_netplay_error(message); } }) .unwrap(); } } Ok(Err(err)) => { panic!("netplay_read_receiver error: {err}"); } Err(_) => { weak.upgrade_in_event_loop(move |handle| { handle.set_pending_session(false); show_netplay_error("Server did not respond".to_string()); }) .unwrap(); } } }); } #[allow(clippy::too_many_arguments)] fn join_session( netplay_write_sender: tokio::sync::broadcast::Sender>, mut netplay_read_receiver: tokio::sync::broadcast::Receiver, player_name: String, game_hash: String, password: String, room_port: i32, weak_app: slint::Weak, weak: slint::Weak, ) { tokio::spawn(async move { let now_utc = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_millis() .to_string(); let hasher = Sha256::new().chain_update(&now_utc).chain_update(EMU_NAME); let join_room = NetplayMessage { message_type: "request_join_room".to_string(), player_name: Some(player_name), client_sha: Some(env!("GIT_DESCRIBE").to_string()), netplay_version: Some(NETPLAY_VERSION), emulator: Some(EMU_NAME.to_string()), accept: None, message: None, rooms: None, player_names: None, auth_time: Some(now_utc), auth: Some( hasher .finalize() .iter() .map(|b| format!("{:02x}", b)) .collect(), ), room: Some(NetplayRoom { room_name: None, password: Some(password), game_name: None, md5: Some(game_hash), protected: None, port: Some(room_port), features: None, buffer_target: None, }), }; netplay_write_sender.send(Some(join_room)).unwrap(); match tokio::time::timeout( std::time::Duration::from_secs(2), netplay_read_receiver.recv(), ) .await { Ok(Ok(message)) => { if message.message_type == "reply_join_room" && message.accept.unwrap() == 0 { weak.upgrade_in_event_loop(move |handle| { let session = message.room.as_ref().unwrap(); let features_default = "false".to_string(); let cheats_default = "{}".to_string(); let overclock = session .features .as_ref() .unwrap() .get("overclock") .unwrap_or(&features_default); let disable_expansion_pak = session .features .as_ref() .unwrap() .get("disable_expansion_pak") .unwrap_or(&features_default); let cheats = session .features .as_ref() .unwrap() .get("cheats") .unwrap_or(&cheats_default); setup_wait_window( netplay_write_sender, netplay_read_receiver, session.room_name.as_ref().unwrap().into(), session.game_name.as_ref().unwrap().into(), handle.get_rom_path(), message.player_name.as_ref().unwrap().into(), session.port.unwrap(), ui::GameSettings { overclock: overclock.parse().unwrap(), disable_expansion_pak: disable_expansion_pak.parse().unwrap(), cheats: serde_json::from_str(cheats).unwrap(), load_savestate_slot: None, }, handle.get_peer_addr(), weak_app, ); handle.hide().unwrap(); }) .unwrap(); } else { weak.upgrade_in_event_loop(move |handle| { handle.set_pending_session(false); if let Some(message) = message.message { show_netplay_error(message); } }) .unwrap(); } } Ok(Err(err)) => { panic!("netplay_read_receiver error: {err}"); } Err(_) => { weak.upgrade_in_event_loop(move |handle| { handle.set_pending_session(false); show_netplay_error("Server did not respond".to_string()); }) .unwrap(); } } }); } #[allow(clippy::too_many_arguments)] fn setup_wait_window( netplay_write_sender: tokio::sync::broadcast::Sender>, mut netplay_read_receiver: tokio::sync::broadcast::Receiver, session_name: slint::SharedString, game_name: slint::SharedString, rom_path: slint::SharedString, player_name: slint::SharedString, port: i32, game_settings: ui::GameSettings, peer_addr: slint::SharedString, weak_app: slint::Weak, ) { let local_player = player_name.clone(); let mut socket_addr: std::net::SocketAddr = peer_addr.to_string().parse().unwrap(); socket_addr.set_port(port as u16); let wait = NetplayWait::new().unwrap(); wait.set_session_name(session_name); wait.set_game_name(game_name); wait.set_rom_path(rom_path); wait.set_port(port); wait.set_ping("Unknown".into()); let sender = netplay_write_sender.clone(); wait.on_send_chat_message(move |message| { let send_chat = NetplayMessage { message_type: "request_chat_message".to_string(), player_name: Some(player_name.to_string()), client_sha: None, netplay_version: None, player_names: None, rooms: None, emulator: None, accept: None, message: Some(message.into()), auth_time: None, auth: None, room: Some(NetplayRoom { room_name: None, password: None, game_name: None, md5: None, protected: None, port: Some(port), features: None, buffer_target: None, }), }; sender.send(Some(send_chat)).unwrap(); }); let sender = netplay_write_sender.clone(); wait.on_begin_game(move || { let begin_game = NetplayMessage { message_type: "request_begin_game".to_string(), player_name: None, client_sha: None, netplay_version: None, player_names: None, rooms: None, emulator: None, accept: None, message: None, auth_time: None, auth: None, room: Some(NetplayRoom { room_name: None, password: None, game_name: None, md5: None, protected: None, port: Some(port), features: None, buffer_target: None, }), }; sender.send(Some(begin_game)).unwrap(); }); let sender = netplay_write_sender.clone(); wait.window().on_close_requested(move || { let _ = sender.send(None); // close current websocket if any slint::CloseRequestResponse::HideWindow }); let motd_message = NetplayMessage { message_type: "request_motd".to_string(), player_name: None, client_sha: None, netplay_version: None, emulator: None, accept: None, rooms: None, player_names: None, message: None, auth_time: None, auth: None, room: None, }; netplay_write_sender.send(Some(motd_message)).unwrap(); let ping_message = NetplayMessage { message_type: "request_ping".to_string(), player_name: None, client_sha: None, netplay_version: None, emulator: None, accept: None, rooms: None, player_names: None, message: None, auth_time: None, auth: None, room: None, }; netplay_write_sender .send(Some(ping_message.clone())) .unwrap(); let weak = wait.as_weak(); tokio::spawn(async move { loop { match netplay_read_receiver.recv().await { Ok(response) => match response.message_type.as_str() { "reply_ping" => { if let Some(message) = response.message { weak.upgrade_in_event_loop(move |handle| { handle.set_ping((message + " ms").into()); }) .unwrap(); } let ping_message = ping_message.clone(); let ping_writer = netplay_write_sender.clone(); tokio::spawn(async move { tokio::time::sleep(std::time::Duration::from_secs(5)).await; let _ = ping_writer.send(Some(ping_message)); }); } "reply_motd" => { let request_players = NetplayMessage { message_type: "request_players".to_string(), player_name: None, client_sha: None, netplay_version: None, player_names: None, emulator: None, rooms: None, accept: None, message: None, auth_time: None, auth: None, room: Some(NetplayRoom { room_name: None, password: None, game_name: None, md5: None, protected: None, port: Some(port), features: None, buffer_target: None, }), }; netplay_write_sender.send(Some(request_players)).unwrap(); weak.upgrade_in_event_loop(move |handle| { #[allow(clippy::regex_creation_in_loops)] let re = regex::Regex::new(r"<[^>]*>").unwrap(); let motd = re .replace_all(response.message.unwrap().as_str(), "") .into_owned(); handle.set_motd(motd.into()); }) .unwrap(); } "reply_players" => { let local_player = local_player.clone(); weak.upgrade_in_event_loop(move |handle| { if let Some(player_names) = response.player_names { if local_player == player_names[0] { handle.set_can_start(true); } else { handle.set_can_start(false); } handle.set_players(slint::ModelRc::from(std::rc::Rc::new( slint::VecModel::from( player_names .iter() .map(|x| x.into()) .collect::>(), ), ))); } }) .unwrap(); } "reply_chat_message" => { weak.upgrade_in_event_loop(move |handle| { let mut chat_text = handle.get_chat_text(); chat_text.push_str(&format!("{}\n", response.message.unwrap())); handle.set_chat_text(chat_text); }) .unwrap(); } "reply_begin_game" => { if response.accept.unwrap() == 0 { weak.upgrade_in_event_loop(move |handle| { let _ = netplay_write_sender.send(None); let mut player_number = 4; let players = handle.get_players(); for (i, player) in players.iter().enumerate() { if player == local_player { player_number = i; } } if player_number > 3 { panic!("Could not determine player number"); } run_rom( handle.get_rom_path().as_str().into(), ui::GameSettings { overclock: game_settings.overclock, disable_expansion_pak: game_settings.disable_expansion_pak, cheats: game_settings.cheats, load_savestate_slot: None, }, Some(NetplayDevice { peer_addr: socket_addr, player_number: player_number as u8, }), weak_app, ); handle.hide().unwrap(); }) .unwrap(); return; } else { let local_player = local_player.clone(); weak.upgrade_in_event_loop(move |handle| { if let Some(p0) = handle.get_players().row_data(0) { if p0 == local_player { handle.set_can_start(true); } else { handle.set_can_start(false); } } if let Some(message) = response.message { show_netplay_error(message); } }) .unwrap(); } } _ => { eprintln!("Unknown netplay message type: {}", response.message_type); } }, Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => { panic!("netplay_read_receiver lagged"); } Err(tokio::sync::broadcast::error::RecvError::Closed) => { break; // exit the loop if the receiver is closed } } } }); wait.show().unwrap(); } pub fn setup_join_window( join_window: &NetplayJoin, rom_dir: slint::SharedString, weak_app: slint::Weak, ) { let (netplay_read_sender, netplay_read_receiver): ( tokio::sync::broadcast::Sender, tokio::sync::broadcast::Receiver, ) = tokio::sync::broadcast::channel(5); let (netplay_write_sender, netplay_write_receiver): ( tokio::sync::broadcast::Sender>, tokio::sync::broadcast::Receiver>, ) = tokio::sync::broadcast::channel(5); join_window.set_pending_refresh(true); join_window.set_rom_dir(rom_dir); populate_server_names(join_window.as_weak()); #[cfg(not(target_os = "android"))] { let weak = join_window.as_weak(); join_window.on_select_rom(move |rom_dir| { select_rom(weak.clone(), rom_dir); }); } let sender = netplay_write_sender.clone(); join_window.window().on_close_requested(move || { let _ = sender.send(None); // close current websocket if any slint::CloseRequestResponse::HideWindow }); let weak = join_window.as_weak(); join_window.on_refresh_session(move || { update_sessions(weak.clone()); }); let weak = join_window.as_weak(); join_window.on_join_session( move |player_name, game_hash, password, room_url, room_port| { let _ = netplay_write_sender.send(None); // close current websocket if any manage_websocket( room_url.to_string(), netplay_read_sender.clone(), netplay_write_receiver.resubscribe(), weak.clone(), ); join_session( netplay_write_sender.clone(), netplay_read_receiver.resubscribe(), player_name.to_string(), game_hash.to_string(), password.to_string(), room_port, weak_app.clone(), weak.clone(), ); }, ); join_window.show().unwrap(); } pub fn netplay_window(app: &AppWindow) { let weak_create = app.as_weak(); let weak_app = app.as_weak(); app.on_create_session_button_clicked(move || { let weak_app = weak_app.clone(); weak_create .upgrade_in_event_loop(move |handle| { let create_window = NetplayCreate::new().unwrap(); save_settings(&handle); setup_create_window( &create_window, ui::GameSettings { overclock: handle.get_overclock_n64_cpu(), disable_expansion_pak: handle.get_disable_expansion_pak(), cheats: std::collections::HashMap::new(), // not used here load_savestate_slot: None, }, handle.get_rom_dir(), weak_app, ); }) .unwrap(); }); let weak_join = app.as_weak(); let weak_app = app.as_weak(); app.on_join_session_button_clicked(move || { let weak_app = weak_app.clone(); weak_join .upgrade_in_event_loop(move |handle| { let join_window = NetplayJoin::new().unwrap(); save_settings(&handle); setup_join_window(&join_window, handle.get_rom_dir(), weak_app); }) .unwrap(); }); app.on_netplay_discord_button_clicked(move || { if let Err(e) = open::that_detached("https://discord.gg/JyW6ZgBUyS") { eprintln!("Error opening Discord: {}", e); } }); app.on_netplay_feedback_button_clicked(move || { if let Err(e) = open::that_detached("https://github.com/gopher64/gopher64/discussions/453") { eprintln!("Error opening feedback: {}", e); } }); }