Files
gopher64/src/ui/netplay.rs
T
Logan McNaughton 86ce5795fa fix netplay start if player 1 leaves (#691)
* fix netplay start if player 1 leaves

* more

* more
2026-03-25 10:56:17 +01:00

1255 lines
50 KiB
Rust

use crate::device;
use crate::ui;
use crate::ui::gui::{
AppWindow, CustomNetplayServer, DispatcherDialog, ErrorDialog, GameSettings, GbPaths,
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<String>,
password: Option<String>,
protected: Option<bool>,
#[serde(rename = "MD5")]
md5: Option<String>,
game_name: Option<String>,
pub port: Option<i32>,
features: Option<std::collections::HashMap<String, String>>,
buffer_target: Option<i32>,
}
#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
pub struct NetplayMessage {
#[serde(rename = "type")]
message_type: String,
player_name: Option<String>,
client_sha: Option<String>,
netplay_version: Option<i32>,
emulator: Option<String>,
room: Option<NetplayRoom>,
accept: Option<i32>,
message: Option<String>,
auth: Option<String>,
player_names: Option<[String; 4]>,
#[serde(rename = "authTime")]
auth_time: Option<String>,
rooms: Option<Vec<NetplayRoom>>,
}
trait NetplayPages {
fn set_server_names(&self, names: slint::ModelRc<slint::SharedString>);
fn set_server_urls(&self, urls: slint::ModelRc<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) {
// Default implementation does nothing
}
}
impl NetplayPages for NetplayCreate {
fn set_server_names(&self, names: slint::ModelRc<slint::SharedString>) {
self.set_server_names(names);
}
fn set_server_urls(&self, urls: slint::ModelRc<slint::SharedString>) {
self.set_server_urls(urls);
}
fn set_game_name(&self, game_name: slint::SharedString) {
self.set_game_name(game_name);
}
fn set_game_hash(&self, game_hash: slint::SharedString) {
self.set_game_hash(game_hash);
}
fn set_game_cheats(&self, game_cheats: slint::SharedString) {
self.set_game_cheats(game_cheats);
}
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<slint::SharedString>) {
self.set_server_names(names);
}
fn set_server_urls(&self, urls: slint::ModelRc<slint::SharedString>) {
self.set_server_urls(urls);
}
fn refresh_sessions(&self) {
self.invoke_refresh_session();
}
fn set_game_name(&self, game_name: slint::SharedString) {
self.set_game_name(game_name);
}
fn set_game_hash(&self, game_hash: slint::SharedString) {
self.set_game_hash(game_hash);
}
fn set_game_cheats(&self, game_cheats: slint::SharedString) {
self.set_game_cheats(game_cheats);
}
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<T: ComponentHandle + NetplayPages + 'static>(weak: slint::Weak<T>) {
let task = reqwest::Client::new()
.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![];
let broadcast_sock = tokio::net::UdpSocket::bind((std::net::Ipv4Addr::UNSPECIFIED, 0))
.await
.unwrap();
broadcast_sock.set_broadcast(true).unwrap();
let data: [u8; 1] = [1];
broadcast_sock
.send_to(&data, (std::net::Ipv4Addr::BROADCAST, 45000))
.await
.unwrap();
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 data: std::collections::HashMap<String, String> =
serde_json::from_slice(&buffer[..result]).unwrap();
for server in data.iter() {
local_servers.push((server.0.into(), server.1.into()));
}
}
let response = task.await;
let public_servers = if let Ok(response) = response
&& let Ok(servers) = response.json::<Vec<String>>().await
{
servers
} else {
vec![]
};
weak.upgrade_in_event_loop(move |handle| {
let server_names: slint::VecModel<slint::SharedString> = slint::VecModel::default();
let server_urls: slint::VecModel<slint::SharedString> = 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();
let server_names_model: std::rc::Rc<slint::VecModel<slint::SharedString>> =
std::rc::Rc::new(server_names);
let server_urls_model: std::rc::Rc<slint::VecModel<slint::SharedString>> =
std::rc::Rc::new(server_urls);
handle.set_server_names(slint::ModelRc::from(server_names_model));
handle.set_server_urls(slint::ModelRc::from(server_urls_model));
})
.unwrap();
});
}
fn select_rom<T: ComponentHandle + NetplayPages + 'static>(
weak: slint::Weak<T>,
rom_dir: slint::SharedString,
) {
let select_rom = if !rom_dir.is_empty()
&& let Ok(exists) = std::fs::exists(&rom_dir)
&& exists
{
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 = "".to_string();
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().window().hide().unwrap();
});
message_dialog.set_text("Could not read ROM".into());
message_dialog.show().unwrap();
handle.set_game_name("".into());
handle.set_game_hash("".into());
handle.set_game_cheats("".into());
handle.set_rom_path("".into());
})
.unwrap();
}
}
});
}
fn show_custom_url_dialog(weak: slint::Weak<NetplayCreate>, 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().window().hide().unwrap();
});
url_dialog.show().unwrap();
}
pub fn setup_create_window(
create_window: &NetplayCreate,
game_settings: GameSettings,
rom_dir: slint::SharedString,
weak_app: slint::Weak<AppWindow>,
) {
let (netplay_read_sender, netplay_read_receiver): (
tokio::sync::broadcast::Sender<NetplayMessage>,
tokio::sync::broadcast::Receiver<NetplayMessage>,
) = tokio::sync::broadcast::channel(5);
let (netplay_write_sender, netplay_write_receiver): (
tokio::sync::broadcast::Sender<Option<NetplayMessage>>,
tokio::sync::broadcast::Receiver<Option<NetplayMessage>>,
) = 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();
});
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 = reqwest::Client::new()
.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.window().hide().unwrap();
})
.unwrap();
if let Ok(response) = response
&& let Ok(server) = response
.json::<std::collections::HashMap<String, String>>()
.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<T: ComponentHandle + NetplayPages + 'static>(
server_url: String,
netplay_read_sender: tokio::sync::broadcast::Sender<NetplayMessage>,
mut netplay_write_receiver: tokio::sync::broadcast::Receiver<Option<NetplayMessage>>,
weak: slint::Weak<T>,
) {
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().window().hide().unwrap();
});
message_dialog.set_text(message.into());
message_dialog.show().unwrap();
}
fn update_sessions(weak: slint::Weak<NetplayJoin>) {
let task = reqwest::Client::new()
.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::<std::collections::HashMap<String, String>>()
.await
{
dispatcher_servers = servers;
}
let weak2 = weak.clone();
weak.upgrade_in_event_loop(move |handle| {
let mut servers: std::collections::HashMap<String, String> =
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 = 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
&& let Ok(message) =
serde_json::from_slice::<NetplayMessage>(&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() {
let session_vec = slint::VecModel::from(session.to_vec());
let session_model: std::rc::Rc<
slint::VecModel<slint::StandardListViewItem>,
> = std::rc::Rc::new(session_vec);
sessions_vec.push(slint::ModelRc::from(session_model));
}
let rooms_model: std::rc::Rc<
slint::VecModel<slint::ModelRc<slint::StandardListViewItem>>,
> = 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<slint::VecModel<slint::SharedString>> =
std::rc::Rc::new(room_urls_vec);
handle.set_room_urls(slint::ModelRc::from(room_urls_model));
let room_ports_vec = slint::VecModel::from(room_ports.to_vec());
let room_ports_model: std::rc::Rc<slint::VecModel<i32>> =
std::rc::Rc::new(room_ports_vec);
handle.set_room_ports(slint::ModelRc::from(room_ports_model));
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<Option<NetplayMessage>>,
mut netplay_read_receiver: tokio::sync::broadcast::Receiver<NetplayMessage>,
session_name: String,
player_name: String,
game_name: String,
game_hash: String,
game_cheats: String,
password: String,
game_settings: GameSettings,
weak_app: slint::Weak<AppWindow>,
weak: slint::Weak<NetplayCreate>,
) {
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 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_HASH").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(format!("{:x}", hasher.finalize())),
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(),
GameSettings {
fullscreen: game_settings.fullscreen,
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.window().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<Option<NetplayMessage>>,
mut netplay_read_receiver: tokio::sync::broadcast::Receiver<NetplayMessage>,
player_name: String,
game_hash: String,
password: String,
room_port: i32,
fullscreen: bool,
weak_app: slint::Weak<AppWindow>,
weak: slint::Weak<NetplayJoin>,
) {
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 join_room = NetplayMessage {
message_type: "request_join_room".to_string(),
player_name: Some(player_name),
client_sha: Some(env!("GIT_HASH").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(format!("{:x}", hasher.finalize())),
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(),
GameSettings {
fullscreen,
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.window().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<Option<NetplayMessage>>,
mut netplay_read_receiver: tokio::sync::broadcast::Receiver<NetplayMessage>,
session_name: slint::SharedString,
game_name: slint::SharedString,
rom_path: slint::SharedString,
player_name: slint::SharedString,
port: i32,
game_settings: GameSettings,
peer_addr: slint::SharedString,
weak_app: slint::Weak<AppWindow>,
) {
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);
}
let players_vec: slint::VecModel<slint::SharedString> =
slint::VecModel::default();
for player in player_names {
players_vec.push(player.into());
}
let players_model: std::rc::Rc<
slint::VecModel<slint::SharedString>,
> = std::rc::Rc::new(players_vec);
handle.set_players(slint::ModelRc::from(players_model));
}
})
.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| {
handle.window().hide().unwrap();
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(
GbPaths {
rom: [None, None, None, None],
ram: [None, None, None, None],
},
handle.get_rom_path().as_str().into(),
GameSettings {
fullscreen: game_settings.fullscreen,
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,
}),
ui::Usb {
usb_tx: None,
cart_rx: None,
},
weak_app,
);
})
.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();
}
}
_ => {
println!("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,
fullscreen: bool,
rom_dir: slint::SharedString,
weak_app: slint::Weak<AppWindow>,
) {
let (netplay_read_sender, netplay_read_receiver): (
tokio::sync::broadcast::Sender<NetplayMessage>,
tokio::sync::broadcast::Receiver<NetplayMessage>,
) = tokio::sync::broadcast::channel(5);
let (netplay_write_sender, netplay_write_receiver): (
tokio::sync::broadcast::Sender<Option<NetplayMessage>>,
tokio::sync::broadcast::Receiver<Option<NetplayMessage>>,
) = 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_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,
fullscreen,
weak_app.clone(),
weak.clone(),
);
},
);
join_window.show().unwrap();
}
pub fn netplay_window(app: &AppWindow, controller_paths: &[Option<String>]) {
let weak_create = app.as_weak();
let weak_app = app.as_weak();
let controller_paths_create = controller_paths.to_owned();
app.on_create_session_button_clicked(move || {
let controller_paths = controller_paths_create.clone();
let weak_app = weak_app.clone();
weak_create
.upgrade_in_event_loop(move |handle| {
let create_window = NetplayCreate::new().unwrap();
save_settings(&handle, &controller_paths);
setup_create_window(
&create_window,
GameSettings {
fullscreen: handle.get_fullscreen(),
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();
let controller_paths_join = controller_paths.to_owned();
app.on_join_session_button_clicked(move || {
let controller_paths = controller_paths_join.clone();
let weak_app = weak_app.clone();
weak_join
.upgrade_in_event_loop(move |handle| {
let join_window = NetplayJoin::new().unwrap();
save_settings(&handle, &controller_paths);
setup_join_window(
&join_window,
handle.get_fullscreen(),
handle.get_rom_dir(),
weak_app,
);
})
.unwrap();
});
app.on_netplay_discord_button_clicked(move || {
open::that_detached("https://discord.gg/JyW6ZgBUyS").unwrap();
});
app.on_netplay_feedback_button_clicked(move || {
open::that_detached("https://github.com/gopher64/gopher64/discussions/453").unwrap();
});
}