diff --git a/README.md b/README.md index 185a257..12c9ff7 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ Keys are mapped according to [these defaults](https://github.com/gopher64/gopher ## netplay -Gopher64 supports netplay (online play with others). It has a few public netplay servers. If you are interested in running a public netplay server, please let me know (open an issue or discussion or contact me on Discord). You can also run the server (https://github.com/gopher64/gopher64-netplay-server) yourself on a LAN. +Gopher64 supports netplay (online play with others) via cloud hosted servers. You can also run the server (https://github.com/gopher64/gopher64-netplay-server) yourself on a LAN. ## portable mode diff --git a/src/ui/gui/appwindow.slint b/src/ui/gui/appwindow.slint index 6718b99..94ab723 100644 --- a/src/ui/gui/appwindow.slint +++ b/src/ui/gui/appwindow.slint @@ -15,6 +15,7 @@ export { NetplayCreate } from "netplay_create.slint"; export { NetplayJoin } from "netplay_join.slint"; export { NetplayWait } from "netplay_wait.slint"; export { CustomNetplayServer } from "netplay_page.slint"; +export { DispatcherDialog } from "netplay_create.slint"; export component AppWindow inherits Window { callback wiki_button_clicked; diff --git a/src/ui/gui/netplay_create.slint b/src/ui/gui/netplay_create.slint index 7fa4a64..bfc92f7 100644 --- a/src/ui/gui/netplay_create.slint +++ b/src/ui/gui/netplay_create.slint @@ -6,7 +6,6 @@ export component NetplayCreate inherits Window { in-out property server_names <=> NetplayData.server_names; in-out property server_urls <=> NetplayData.server_urls; in-out property custom_server_url <=> NetplayData.custom_server_url; - in-out property ping <=> NetplayData.ping; in-out property game_name <=> NetplayData.game_name; in-out property game_hash <=> NetplayData.game_hash; in-out property game_cheats <=> NetplayData.game_cheats; @@ -14,7 +13,6 @@ export component NetplayCreate inherits Window { in-out property rom_path <=> NetplayData.rom_path; in-out property peer_addr <=> NetplayData.peer_addr; in-out property rom_dir <=> NetplayData.rom_dir; - callback get_ping(string); callback get_custom_url(); callback select_rom(string); callback create_session(string, string, string, string, string, string, string); @@ -73,17 +71,10 @@ export component NetplayCreate inherits Window { if (self.current-index >= 0) { if (self.current-value == "Custom") { get_custom_url(); - } else { - get_ping(NetplayData.server_urls[self.current-index]); } } } } - - Text { - vertical-alignment: center; - text: NetplayData.ping; - } } } @@ -130,3 +121,10 @@ export component NetplayCreate inherits Window { } } } + +export component DispatcherDialog inherits Dialog { + title: @tr("Creating Server"); + Text { + text: "Creating server, please wait...This may take about 30 seconds."; + } +} diff --git a/src/ui/gui/netplay_join.slint b/src/ui/gui/netplay_join.slint index d7fd8a4..cf62c93 100644 --- a/src/ui/gui/netplay_join.slint +++ b/src/ui/gui/netplay_join.slint @@ -6,22 +6,21 @@ export component NetplayJoin inherits Window { in-out property server_names <=> NetplayData.server_names; in-out property server_urls <=> NetplayData.server_urls; in-out property custom_server_url <=> NetplayData.custom_server_url; - in-out property ping <=> NetplayData.ping; in-out property sessions <=> NetplayData.sessions; in-out property current_session <=> NetplayData.current_session; - in-out property ports <=> NetplayData.ports; + in-out property room_urls <=> NetplayData.room_urls; in-out property game_name <=> NetplayData.game_name; in-out property game_hash <=> NetplayData.game_hash; in-out property game_cheats <=> NetplayData.game_cheats; in-out property peer_addr <=> NetplayData.peer_addr; in-out property pending_session <=> NetplayData.pending_session; + in-out property pending_refresh <=> NetplayData.pending_refresh; in-out property rom_path <=> NetplayData.rom_path; in-out property rom_dir <=> NetplayData.rom_dir; - callback get_ping(string); - callback get_custom_url(); - callback refresh_session(string); + callback refresh_session(); callback select_rom(string); - callback join_session(string, string, string, int); + callback join_session(string, string, string, string, string); + preferred-width: 700px; VerticalBox { HorizontalBox { @@ -40,26 +39,14 @@ export component NetplayJoin inherits Window { VerticalBox { Text { - text: @tr("Server:"); + text: @tr("Custom Server URL (optional):"); } - HorizontalBox { - server := ComboBox { - model: NetplayData.server_names; - selected => { - if (self.current-index >= 0) { - if (self.current-value == "Custom") { - get_custom_url(); - } else { - get_ping(NetplayData.server_urls[self.current-index]); - } - } - } - } - - Text { - vertical-alignment: center; - text: NetplayData.ping; + LineEdit { + placeholder-text: @tr("example.com:45000"); + text: NetplayData.custom_server_url; + edited(text) => { + NetplayData.custom_server_url = text; } } } @@ -71,12 +58,13 @@ export component NetplayJoin inherits Window { vertical-alignment: center; } if NetplayData.sessions.length == 0: Text { - text: @tr("No sessions available. Please refresh or try a different server."); + text: @tr("No sessions available. Please refresh to check again."); horizontal-alignment: center; vertical-alignment: center; } StandardTableView { columns: [ + { title: "Server" }, { title: "Session Name" }, { title: "Game Name" }, { title: "Password" }, @@ -93,13 +81,14 @@ export component NetplayJoin inherits Window { Rectangle { } Button { + enabled: !NetplayData.pending_refresh; text: @tr("Refresh Session List"); clicked => { - if (server.current-value == "Custom") { - refresh_session("ws://" + NetplayData.custom_server_url); - } else { - refresh_session(NetplayData.server_urls[server.current-index]); - } + NetplayData.pending_refresh = true; + NetplayData.current_session = -1; + NetplayData.sessions = []; + NetplayData.room_urls = []; + refresh_session(); } } } @@ -134,7 +123,8 @@ export component NetplayJoin inherits Window { NetplayData.player_name, NetplayData.game_hash, NetplayData.password, - NetplayData.ports[NetplayData.current_session]); + NetplayData.room_urls[NetplayData.current_session], + NetplayData.sessions[NetplayData.current_session][1].text); } } } diff --git a/src/ui/gui/netplay_page.slint b/src/ui/gui/netplay_page.slint index b742a25..d767f79 100644 --- a/src/ui/gui/netplay_page.slint +++ b/src/ui/gui/netplay_page.slint @@ -7,7 +7,6 @@ import { Page } from "page.slint"; export global NetplayData { in-out property <[string]> server_names; in-out property <[string]> server_urls; - in-out property ping: "Ping: Unknown"; in-out property player_name; in-out property game_name; in-out property game_hash; @@ -16,9 +15,10 @@ export global NetplayData { in-out property password; in-out property peer_addr; in-out property pending_session; + in-out property pending_refresh; in-out property session_name; in-out property <[[StandardListViewItem]]> sessions; - in-out property <[int]> ports; + in-out property <[string]> room_urls; in-out property current_session: -1; in-out property rom_dir; in-out property custom_server_url; diff --git a/src/ui/gui/netplay_wait.slint b/src/ui/gui/netplay_wait.slint index c0d6912..18dcff2 100644 --- a/src/ui/gui/netplay_wait.slint +++ b/src/ui/gui/netplay_wait.slint @@ -7,6 +7,7 @@ export global NetplayWaitData { in-out property port; in-out property can_start; in-out property chat_text; + in-out property ping; } export component NetplayWait inherits Window { @@ -21,6 +22,7 @@ export component NetplayWait inherits Window { in-out property port <=> NetplayWaitData.port; in-out property can_start <=> NetplayWaitData.can_start; in-out property chat_text <=> NetplayWaitData.chat_text; + in-out property ping <=> NetplayWaitData.ping; callback send_chat_message(string); callback begin_game; @@ -37,6 +39,11 @@ export component NetplayWait inherits Window { horizontal-alignment: center; } + Text { + text: @tr("Ping: ") + NetplayWaitData.ping; + horizontal-alignment: center; + } + GridBox { Row { Text { diff --git a/src/ui/netplay.rs b/src/ui/netplay.rs index c86f89f..d3c2525 100644 --- a/src/ui/netplay.rs +++ b/src/ui/netplay.rs @@ -1,8 +1,8 @@ use crate::device; use crate::ui; use crate::ui::gui::{ - AppWindow, CustomNetplayServer, ErrorDialog, GameSettings, GbPaths, NetplayCreate, - NetplayDevice, NetplayJoin, NetplayWait, VruChannel, run_rom, save_settings, + AppWindow, CustomNetplayServer, DispatcherDialog, ErrorDialog, GameSettings, GbPaths, + NetplayCreate, NetplayDevice, NetplayJoin, NetplayWait, VruChannel, run_rom, save_settings, }; use futures::{SinkExt, StreamExt}; use sha2::{Digest, Sha256}; @@ -47,17 +47,14 @@ pub struct NetplayMessage { trait NetplayPages { fn set_server_names(&self, names: slint::ModelRc); fn set_server_urls(&self, urls: slint::ModelRc); - fn set_ping(&self, ping: slint::SharedString); fn set_game_name(&self, game_name: slint::SharedString); fn set_game_hash(&self, game_hash: slint::SharedString); fn set_game_cheats(&self, game_cheats: slint::SharedString); fn set_rom_path(&self, rom_path: slint::SharedString); fn set_peer_addr(&self, peer_addr: slint::SharedString); - fn refresh_sessions(&self, _server: slint::SharedString) { + fn refresh_sessions(&self) { // Default implementation does nothing } - fn invoke_get_ping(&self, server_url: slint::SharedString); - fn set_custom_server_url(&self, server_url: slint::SharedString); } impl NetplayPages for NetplayCreate { @@ -67,9 +64,6 @@ impl NetplayPages for NetplayCreate { fn set_server_urls(&self, urls: slint::ModelRc) { self.set_server_urls(urls); } - fn set_ping(&self, ping: slint::SharedString) { - self.set_ping(ping); - } fn set_game_name(&self, game_name: slint::SharedString) { self.set_game_name(game_name); } @@ -85,12 +79,6 @@ impl NetplayPages for NetplayCreate { fn set_peer_addr(&self, peer_addr: slint::SharedString) { self.set_peer_addr(peer_addr); } - fn invoke_get_ping(&self, server_url: slint::SharedString) { - self.invoke_get_ping(server_url); - } - fn set_custom_server_url(&self, server_url: slint::SharedString) { - self.set_custom_server_url(server_url); - } } impl NetplayPages for NetplayJoin { @@ -100,11 +88,8 @@ impl NetplayPages for NetplayJoin { fn set_server_urls(&self, urls: slint::ModelRc) { self.set_server_urls(urls); } - fn set_ping(&self, ping: slint::SharedString) { - self.set_ping(ping); - } - fn refresh_sessions(&self, server: slint::SharedString) { - self.invoke_refresh_session(server); + fn refresh_sessions(&self) { + self.invoke_refresh_session(); } fn set_game_name(&self, game_name: slint::SharedString) { self.set_game_name(game_name); @@ -121,16 +106,13 @@ impl NetplayPages for NetplayJoin { fn set_peer_addr(&self, peer_addr: slint::SharedString) { self.set_peer_addr(peer_addr); } - fn invoke_get_ping(&self, server_url: slint::SharedString) { - self.invoke_get_ping(server_url); - } - fn set_custom_server_url(&self, server_url: slint::SharedString) { - self.set_custom_server_url(server_url); - } } fn populate_server_names(weak: slint::Weak) { - let task = reqwest::get("https://cdn.gopher64.com/servers-gopher64.json"); + let task = reqwest::Client::new() + .get("https://dispatch.gopher64.com/getRegions") + .header("netplay-id", EMU_NAME) + .send(); tokio::spawn(async move { let mut local_servers: Vec<(String, String)> = vec![]; @@ -158,10 +140,9 @@ fn populate_server_names(weak: slin } let response = task.await; - if let Ok(response) = response { - let servers: std::collections::HashMap = response.json().await.unwrap(); - - let weak2 = weak.clone(); + if let Ok(response) = response + && let Ok(servers) = response.json::>().await + { weak.upgrade_in_event_loop(move |handle| { let server_names: slint::VecModel = slint::VecModel::default(); let server_urls: slint::VecModel = slint::VecModel::default(); @@ -170,12 +151,11 @@ fn populate_server_names(weak: slin server_urls.push(local_server.1.into()); } for server in servers { - server_names.push(server.0.into()); - server_urls.push(server.1.into()); + server_names.push(server.clone().into()); + server_urls.push(format!("dispatcher:{server}").into()); } server_names.push("Custom".into()); - update_ping(weak2, server_urls.row_data(0).unwrap().into()); - handle.refresh_sessions(server_urls.row_data(0).unwrap()); + handle.refresh_sessions(); let server_names_model: std::rc::Rc> = std::rc::Rc::new(server_names); let server_urls_model: std::rc::Rc> = @@ -245,48 +225,13 @@ fn select_rom( }); } -fn update_ping( - weak: slint::Weak, - server_url: String, -) { - weak.upgrade_in_event_loop(move |handle| { - handle.set_ping("Ping: Unknown".into()); - }) - .unwrap(); - tokio::spawn(async move { - if let Ok(Ok((mut sock, _response))) = tokio::time::timeout( - std::time::Duration::from_secs(1), - tokio_tungstenite::connect_async(server_url), - ) - .await - { - sock.send(Message::Ping(Vec::new().into())).await.unwrap(); - let start = std::time::Instant::now(); - - if let Some(Ok(_response)) = sock.next().await { - let elapsed = start.elapsed(); - weak.upgrade_in_event_loop(move |handle| { - handle.set_ping(format!("Ping: {:.0} ms", elapsed.as_millis()).into()); - }) - .unwrap(); - } - sock.close(None).await.unwrap(); - } - }); -} - -fn show_custom_url_dialog( - weak: slint::Weak, - server_url: slint::SharedString, -) { +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()); - let prefix: slint::SharedString = "ws://".into(); - handle.invoke_get_ping(prefix + &server_url); }) .unwrap(); weak_dialog.unwrap().window().hide().unwrap(); @@ -313,14 +258,9 @@ pub fn setup_create_window( create_window.set_rom_dir(rom_dir); populate_server_names(create_window.as_weak()); let weak = create_window.as_weak(); - create_window.on_get_ping(move |server_url| { - update_ping(weak.clone(), server_url.to_string()); - }); - let weak = create_window.as_weak(); create_window.on_get_custom_url(move || { let weak2 = weak.clone(); weak.upgrade_in_event_loop(move |handle| { - handle.set_ping("Ping: Unknown".into()); show_custom_url_dialog(weak2, handle.get_custom_server_url()); }) .unwrap(); @@ -340,26 +280,87 @@ pub fn setup_create_window( game_cheats, password| { let _ = netplay_write_sender.send(None); // close current websocket if any - manage_websocket( - server_url.to_string(), - netplay_read_sender.clone(), - netplay_write_receiver.resubscribe(), - weak.clone(), - ); + if server_url.starts_with("dispatcher:") { + let message_dialog = DispatcherDialog::new().unwrap(); + let weak_dialog = message_dialog.as_weak(); + message_dialog.show().unwrap(); - 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(), - ); + let task = reqwest::Client::new() + .get("https://dispatch.gopher64.com/createServer") + .query(&[("region", server_url.strip_prefix("dispatcher:").unwrap())]) + .header("netplay-id", EMU_NAME) + .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.window().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(), + ); + } }, ); @@ -436,116 +437,159 @@ fn show_netplay_error(message: String) { message_dialog.show().unwrap(); } -fn clear_sessions(handle: &NetplayJoin, message: Option) { - handle.set_sessions(slint::ModelRc::default()); - handle.set_ports(slint::ModelRc::default()); - handle.set_current_session(-1); - if let Some(message) = message { - show_netplay_error(message); - } -} - -fn update_sessions( - netplay_write_sender: tokio::sync::broadcast::Sender>, - mut netplay_read_receiver: tokio::sync::broadcast::Receiver, - weak: slint::Weak, -) { +fn update_sessions(weak: slint::Weak) { + let task = reqwest::Client::new() + .get("https://dispatch.gopher64.com/getServers") + .header("netplay-id", EMU_NAME) + .send(); tokio::spawn(async move { - 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, - }; - - netplay_write_sender.send(Some(request_rooms)).unwrap(); - - match tokio::time::timeout( - std::time::Duration::from_secs(2), - netplay_read_receiver.recv(), - ) - .await + let mut dispatcher_servers = std::collections::HashMap::new(); + let response = task.await; + if let Ok(response) = response + && let Ok(servers) = response + .json::>() + .await { - Ok(Ok(message)) => { - if message.accept.unwrap() == 0 { - if let Some(rooms) = message.rooms { - weak.upgrade_in_event_loop(move |handle| { - let sessions_vec = slint::VecModel::default(); - let ports_vec = slint::VecModel::default(); - for room in rooms { - let session_vec = slint::VecModel::default(); - session_vec.push(slint::StandardListViewItem::from( - slint::SharedString::from(room.room_name.unwrap()), - )); - session_vec.push(slint::StandardListViewItem::from( - slint::SharedString::from(room.game_name.unwrap()), - )); - session_vec.push(slint::StandardListViewItem::from( - slint::SharedString::from(if room.protected.unwrap() { - "True" - } else { - "False" - }), - )); - session_vec.push(slint::StandardListViewItem::from( - slint::SharedString::from( - if room.features.unwrap_or_default().contains_key("cheats") - { - "True" - } else { - "False" - }, - ), - )); - let session_model: std::rc::Rc< - slint::VecModel, - > = std::rc::Rc::new(session_vec); - sessions_vec.push(slint::ModelRc::from(session_model)); - ports_vec.push(room.port.unwrap()); - } - let rooms_model: std::rc::Rc< - slint::VecModel>, - > = std::rc::Rc::new(sessions_vec); - let ports_model: std::rc::Rc> = - std::rc::Rc::new(ports_vec); - handle.set_sessions(slint::ModelRc::from(rooms_model)); - handle.set_ports(slint::ModelRc::from(ports_model)); - handle.set_current_session(-1); - }) - .unwrap(); - } else { - weak.upgrade_in_event_loop(move |handle| { - clear_sessions(&handle, None); - }) - .unwrap(); + 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 { - weak.upgrade_in_event_loop(move |handle| { - clear_sessions(&handle, message.message); - }) - .unwrap(); + servers.insert( + server_name.to_string(), + server_urls.row_data(i).unwrap().to_string(), + ); } } - Ok(Err(err)) => { - panic!("netplay_read_receiver error: {err}"); - } - Err(_) => { - weak.upgrade_in_event_loop(move |handle| { - clear_sessions(&handle, Some("Server did not respond".to_string())); - }) - .unwrap(); - } - } + servers.extend(dispatcher_servers); + + tokio::spawn(async move { + let mut sessions = vec![]; + let mut room_urls = 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 = 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, + }; + write + .send(Message::Binary(Bytes::from( + serde_json::to_vec(&request_rooms).unwrap(), + ))) + .await + .unwrap(); + + if let Some(Ok(response)) = read.next().await { + if let Ok(message) = + serde_json::from_slice::(&response.into_data()) + { + if 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()); + + 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() { + let session_vec = slint::VecModel::from(session.to_vec()); + let session_model: std::rc::Rc< + slint::VecModel, + > = std::rc::Rc::new(session_vec); + sessions_vec.push(slint::ModelRc::from(session_model)); + } + let rooms_model: std::rc::Rc< + slint::VecModel>, + > = std::rc::Rc::new(sessions_vec); + handle.set_sessions(slint::ModelRc::from(rooms_model)); + + let room_urls_vec = slint::VecModel::from(room_urls.to_vec()); + let room_urls_model: std::rc::Rc> = + std::rc::Rc::new(room_urls_vec); + handle.set_room_urls(slint::ModelRc::from(room_urls_model)); + + handle.set_current_session(-1); + handle.set_pending_refresh(false); + }) + .unwrap(); + }); + }) + .unwrap(); }); } @@ -611,7 +655,7 @@ fn create_session( .await { Ok(Ok(message)) => { - if message.accept.unwrap() == 0 { + 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(); @@ -686,12 +730,62 @@ fn join_session( player_name: String, game_hash: String, password: String, - port: i32, + room_name: String, fullscreen: bool, weak_app: slint::Weak, weak: slint::Weak, ) { tokio::spawn(async move { + 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, + }; + + netplay_write_sender.send(Some(request_rooms)).unwrap(); + + let mut port = 0; + match tokio::time::timeout( + std::time::Duration::from_secs(2), + netplay_read_receiver.recv(), + ) + .await + { + Ok(Ok(message)) => { + if message.message_type == "reply_get_rooms" + && message.accept.unwrap() == 0 + && let Some(rooms) = message.rooms + { + for room in rooms { + if room.room_name.unwrap() == room_name { + port = room.port.unwrap_or(0); + } + } + } + } + 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(); + } + } + let join_room = NetplayMessage { message_type: "request_join_room".to_string(), player_name: Some(player_name), @@ -725,7 +819,7 @@ fn join_session( .await { Ok(Ok(message)) => { - if message.accept.unwrap() == 0 { + 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(); @@ -817,6 +911,7 @@ fn setup_wait_window( wait.set_rom_path(rom_path); wait.set_port(port); wait.set_can_start(can_start); + wait.set_ping("Unknown".into()); let sender = netplay_write_sender.clone(); wait.on_send_chat_message(move |message| { @@ -897,11 +992,44 @@ fn setup_wait_window( 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(), @@ -1047,26 +1175,10 @@ pub fn setup_join_window( 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()); let weak = join_window.as_weak(); - join_window.on_get_ping(move |server_url| { - update_ping(weak.clone(), server_url.to_string()); - weak.upgrade_in_event_loop(move |handle| { - handle.invoke_refresh_session(server_url); - }) - .unwrap(); - }); - let weak = join_window.as_weak(); - join_window.on_get_custom_url(move || { - let weak2 = weak.clone(); - weak.upgrade_in_event_loop(move |handle| { - handle.set_ping("Ping: Unknown".into()); - show_custom_url_dialog(weak2, handle.get_custom_server_url()); - }) - .unwrap(); - }); - let weak = join_window.as_weak(); join_window.on_select_rom(move |rom_dir| { select_rom(weak.clone(), rom_dir); }); @@ -1076,34 +1188,34 @@ pub fn setup_join_window( 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_name| { + 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(), + ); - let weak = join_window.as_weak(); - let sender = netplay_write_sender.clone(); - let receiver = netplay_read_receiver.resubscribe(); - join_window.on_refresh_session(move |server_url| { - let _ = sender.send(None); // close current websocket if any - manage_websocket( - server_url.to_string(), - netplay_read_sender.clone(), - netplay_write_receiver.resubscribe(), - weak.clone(), - ); - update_sessions(sender.clone(), receiver.resubscribe(), weak.clone()); - }); - let weak = join_window.as_weak(); - join_window.on_join_session(move |player_name, game_hash, password, port| { - join_session( - netplay_write_sender.clone(), - netplay_read_receiver.resubscribe(), - player_name.to_string(), - game_hash.to_string(), - password.to_string(), - port, - fullscreen, - weak_app.clone(), - weak.clone(), - ); - }); + join_session( + netplay_write_sender.clone(), + netplay_read_receiver.resubscribe(), + player_name.to_string(), + game_hash.to_string(), + password.to_string(), + room_name.to_string(), + fullscreen, + weak_app.clone(), + weak.clone(), + ); + }, + ); join_window.show().unwrap(); }