diff --git a/Cargo.toml b/Cargo.toml index 81254f5..c94f1e2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,7 @@ sdl2-sys = "0.37" spin_sleep = "1.3" encoding_rs = "0.8" mimalloc = "0.1" +regex = "1.11" tungstenite = "0.26" clap = { version = "4.4", features = ["derive"] } diff --git a/src/ui/gui.rs b/src/ui/gui.rs index 0fb66cd..038edca 100644 --- a/src/ui/gui.rs +++ b/src/ui/gui.rs @@ -108,23 +108,7 @@ impl GopherEguiApp { vru_window_receiver: None, vru_word_notifier: None, vru_word_list: Vec::new(), - netplay: netplay::Netplay { - error: "".to_string(), - broadcast_socket: None, - broadcast_timer: None, - server_receiver: None, - game_info_receiver: None, - rom_label: "Open ROM".to_string(), - create: false, - join: false, - wait: false, - game_info: ("".to_string(), "".to_string(), "".to_string()), - session_name: "".to_string(), - password: "".to_string(), - player_name: "".to_string(), - server: ("".to_string(), "".to_string()), - servers: std::collections::HashMap::new(), - }, + netplay: Default::default(), } } } @@ -271,6 +255,9 @@ fn open_rom(app: &mut GopherEguiApp, ctx: &egui::Context) { if emulate_vru { app.vru_window_receiver = Some(vru_window_receiver); app.vru_word_notifier = Some(vru_word_notifier); + } else { + app.vru_window_receiver = None; + app.vru_word_notifier = None; } let gui_ctx = ctx.clone(); diff --git a/src/ui/gui/netplay.rs b/src/ui/gui/netplay.rs index b713014..f957dcd 100644 --- a/src/ui/gui/netplay.rs +++ b/src/ui/gui/netplay.rs @@ -5,6 +5,8 @@ use sha2::{Digest, Sha256}; const NETPLAY_VERSION: i32 = 17; const EMU_NAME: &str = "gopher64"; + +#[derive(Default)] pub struct Netplay { pub create: bool, pub join: bool, @@ -14,9 +16,20 @@ pub struct Netplay { pub player_name: String, pub error: String, pub rom_label: String, + pub send_chat: bool, + pub begin_game: bool, + pub chat_log: String, + pub chat_message: String, + pub pending_begin: bool, + pub motd: String, + pub player_names: [String; 4], pub server: (String, String), + pub socket_waiting: bool, pub game_info: (String, String, String), pub servers: std::collections::HashMap, + pub waiting_session: Option, + pub socket: + Option>>, pub server_receiver: Option>>, pub game_info_receiver: Option>, @@ -24,18 +37,18 @@ pub struct Netplay { pub broadcast_timer: Option, } -#[derive(serde::Serialize, serde::Deserialize, std::fmt::Debug)] +#[derive(serde::Serialize, serde::Deserialize)] pub struct NetplayRoom { - room_name: String, + room_name: Option, password: Option, protected: Option, #[serde(rename = "MD5")] - md5: String, - game_name: String, + md5: Option, + game_name: Option, port: Option, } -#[derive(serde::Serialize, serde::Deserialize, std::fmt::Debug)] +#[derive(serde::Serialize, serde::Deserialize)] pub struct NetplayMessage { #[serde(rename = "type")] message_type: String, @@ -47,13 +60,14 @@ pub struct NetplayMessage { accept: Option, message: Option, auth: Option, + player_names: Option<[String; 4]>, #[serde(rename = "authTime")] auth_time: Option, } pub fn netplay_create(app: &mut GopherEguiApp, ctx: &egui::Context) { egui::Window::new("Create Netplay Session").show(ctx, |ui| { - egui::Grid::new("button_grid").show(ui, |ui| { + egui::Grid::new("netplay_create_grid").show(ui, |ui| { let profile_name_label = ui.label("Session Name:"); let mut size = ui.spacing().interact_size; size.x = 200.0; @@ -72,7 +86,10 @@ pub fn netplay_create(app: &mut GopherEguiApp, ctx: &egui::Context) { ui.end_row(); ui.label("ROM"); - if ui.button(app.netplay.rom_label.clone()).clicked() { + if app.netplay.rom_label.is_empty() { + app.netplay.rom_label = "Open ROM".to_string(); + } + if ui.button(&app.netplay.rom_label).clicked() { let task = rfd::AsyncFileDialog::new().pick_file(); let (tx, rx) = tokio::sync::mpsc::channel(1); app.netplay.game_info_receiver = Some(rx); @@ -223,6 +240,26 @@ pub fn netplay_create(app: &mut GopherEguiApp, ctx: &egui::Context) { ui.end_row(); + if app.netplay.socket_waiting { + let data = app.netplay.socket.as_mut().unwrap().read(); + if data.is_ok() { + let message: NetplayMessage = + serde_json::from_slice(&data.unwrap().into_data()).unwrap(); + if message.accept.unwrap() == 0 { + if message.message_type == "reply_create_room" { + app.netplay.create = false; + app.netplay.wait = true; + app.netplay.waiting_session = Some(message.room.unwrap()); + } + } else { + app.netplay.error = message.message.unwrap(); + } + app.netplay.socket_waiting = false; + } + ctx.request_repaint(); + } + }); + ui.horizontal(|ui| { if ui.button("Create Session").clicked() { if app.netplay.player_name.is_empty() { app.netplay.error = "Player Name cannot be empty".to_string(); @@ -232,9 +269,12 @@ pub fn netplay_create(app: &mut GopherEguiApp, ctx: &egui::Context) { app.netplay.error = "ROM not loaded".to_string(); } else { let now_utc = chrono::Utc::now().timestamp_millis().to_string(); - let hasher = Sha256::new() - .chain_update(now_utc.clone()) - .chain_update(EMU_NAME); + let hasher = Sha256::new().chain_update(&now_utc).chain_update(EMU_NAME); + let mut game_name = app.netplay.game_info.1.trim().to_string(); + if game_name.is_empty() { + // If the ROM doesn't report a name, use the filename + game_name = app.netplay.rom_label.clone(); + } let netplay_message = NetplayMessage { message_type: "request_create_room".to_string(), player_name: Some(app.netplay.player_name.clone()), @@ -244,36 +284,39 @@ pub fn netplay_create(app: &mut GopherEguiApp, ctx: &egui::Context) { accept: None, message: None, auth_time: Some(now_utc), + player_names: None, auth: Some(format!("{:x}", hasher.finalize())), room: Some(NetplayRoom { - room_name: app.netplay.session_name.clone(), + room_name: Some(app.netplay.session_name.clone()), password: Some(app.netplay.password.clone()), - game_name: app.netplay.game_info.1.trim().to_string(), - md5: app.netplay.game_info.0.clone(), + game_name: Some(game_name), + md5: Some(app.netplay.game_info.0.clone()), protected: None, port: None, }), }; let (mut socket, _response) = - tungstenite::connect(app.netplay.server.1.clone()).expect("Can't connect"); + tungstenite::connect(&app.netplay.server.1).expect("Can't connect"); socket .send(tungstenite::Message::Binary(tungstenite::Bytes::from( serde_json::to_vec(&netplay_message).unwrap(), ))) .unwrap(); - let data = socket.read().unwrap().into_data(); - let message: NetplayMessage = serde_json::from_slice(data.as_ref()).unwrap(); - if message.accept.unwrap() == 0 { - app.netplay.create = false; - app.netplay.wait = true; - } else { - app.netplay.error = message.message.unwrap(); + match socket.get_mut() { + tungstenite::stream::MaybeTlsStream::Plain(stream) => { + stream.set_nonblocking(true) + } + _ => unimplemented!(), } + .expect("could not set socket to non-blocking"); + app.netplay.socket_waiting = true; + app.netplay.socket = Some(socket); } } + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { if ui.button("Close").clicked() { - app.netplay.create = false + app.netplay = Default::default(); }; }) }); @@ -282,17 +325,232 @@ pub fn netplay_create(app: &mut GopherEguiApp, ctx: &egui::Context) { pub fn netplay_join(app: &mut GopherEguiApp, ctx: &egui::Context) { egui::Window::new("Join Netplay Session").show(ctx, |ui| { - if ui.button("Close").clicked() { - app.netplay.join = false - }; + egui::Grid::new("netplay_join_grid").show(ui, |_ui| {}); + ui.horizontal(|ui| { + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + if ui.button("Close").clicked() { + app.netplay = Default::default(); + }; + }); + }); }); } pub fn netplay_wait(app: &mut GopherEguiApp, ctx: &egui::Context) { - egui::Window::new("Pending Netplay Session").show(ctx, |ui| { - if ui.button("Close").clicked() { - app.netplay.wait = false + let motd_message = NetplayMessage { + message_type: "request_motd".to_string(), + player_name: None, + client_sha: None, + netplay_version: None, + emulator: None, + accept: None, + player_names: None, + message: None, + auth_time: None, + auth: None, + room: None, + }; + let request_players = NetplayMessage { + message_type: "request_players".to_string(), + player_name: None, + client_sha: None, + netplay_version: None, + player_names: 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: app.netplay.waiting_session.as_ref().unwrap().port, + }), + }; + let socket = app.netplay.socket.as_mut().unwrap(); + if !app.netplay.socket_waiting { + socket + .send(tungstenite::Message::Binary(tungstenite::Bytes::from( + serde_json::to_vec(&motd_message).unwrap(), + ))) + .unwrap(); + socket + .send(tungstenite::Message::Binary(tungstenite::Bytes::from( + serde_json::to_vec(&request_players).unwrap(), + ))) + .unwrap(); + app.netplay.socket_waiting = true; + } + + if app.netplay.begin_game { + let begin_game = NetplayMessage { + message_type: "request_begin_game".to_string(), + player_name: None, + client_sha: None, + netplay_version: None, + player_names: 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: app.netplay.waiting_session.as_ref().unwrap().port, + }), }; + socket + .send(tungstenite::Message::Binary(tungstenite::Bytes::from( + serde_json::to_vec(&begin_game).unwrap(), + ))) + .unwrap(); + app.netplay.begin_game = false; + } + + if app.netplay.send_chat { + let send_chat = NetplayMessage { + message_type: "request_chat_message".to_string(), + player_name: Some(app.netplay.player_name.clone()), + client_sha: None, + netplay_version: None, + player_names: None, + emulator: None, + accept: None, + message: Some(app.netplay.chat_message.clone()), + auth_time: None, + auth: None, + room: Some(NetplayRoom { + room_name: None, + password: None, + game_name: None, + md5: None, + protected: None, + port: app.netplay.waiting_session.as_ref().unwrap().port, + }), + }; + app.netplay.chat_message.clear(); + socket + .send(tungstenite::Message::Binary(tungstenite::Bytes::from( + serde_json::to_vec(&send_chat).unwrap(), + ))) + .unwrap(); + app.netplay.send_chat = false; + } + + if app.netplay.socket_waiting { + let data = socket.read(); + if data.is_ok() { + let message: NetplayMessage = + serde_json::from_slice(&data.unwrap().into_data()).unwrap(); + if message.accept.unwrap() == 0 { + if message.message_type == "reply_motd" { + let re = regex::Regex::new(r"<[^>]*>").unwrap(); + app.netplay.motd = re + .replace_all(message.message.unwrap().as_str(), "") + .into_owned(); + } else if message.message_type == "reply_players" { + app.netplay.player_names = message.player_names.unwrap(); + } else if message.message_type == "reply_chat_message" { + app.netplay.chat_log.push_str(&message.message.unwrap()); + app.netplay.chat_log.push('\n'); + } else if message.message_type == "reply_begin_game" { + app.netplay = Default::default(); + return; + } + } else { + app.netplay.error = message.message.unwrap(); + } + } + ctx.request_repaint(); + } + + egui::Window::new("Pending Netplay Session").show(ctx, |ui| { + egui::Grid::new("netplay_wait_grid_1").show(ui, |ui| { + ui.label("Session Name:"); + let room_name = app + .netplay + .waiting_session + .as_ref() + .unwrap() + .room_name + .as_ref() + .unwrap(); + let game_name = app + .netplay + .waiting_session + .as_ref() + .unwrap() + .game_name + .as_ref() + .unwrap(); + ui.label(room_name); + ui.end_row(); + ui.label("Game Name:"); + ui.label(game_name); + ui.end_row(); + for i in 0..4 { + ui.label(format!("Player {}:", i + 1)); + ui.label(app.netplay.player_names[i].clone()); + ui.end_row(); + } + }); + egui::ScrollArea::vertical() + .max_height(100.0) + .stick_to_bottom(true) + .show(ui, |ui| { + ui.add( + egui::TextEdit::multiline(&mut app.netplay.chat_log) + .interactive(false) + .desired_width(ui.available_width()), + ); + }); + + ui.horizontal(|ui| { + let mut size = ui.spacing().interact_size; + size.x = 200.0; + + ui.add_sized( + size, + egui::TextEdit::singleline(&mut app.netplay.chat_message) + .hint_text("Enter chat message here"), + ); + + if ui + .add_enabled(!app.netplay.send_chat, egui::Button::new("Send Message")) + .clicked() + && !app.netplay.chat_message.is_empty() + { + app.netplay.send_chat = true; + } + }); + + ui.add_space(16.0); + ui.label(app.netplay.motd.clone()); + ui.add_space(16.0); + + ui.horizontal(|ui| { + let button_enabled = app.netplay.player_name == app.netplay.player_names[0] + && !app.netplay.pending_begin; + if ui + .add_enabled(button_enabled, egui::Button::new("Start Session")) + .clicked() + { + app.netplay.begin_game = true; + app.netplay.pending_begin = true; + } + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + if ui.button("Close").clicked() { + app.netplay = Default::default(); + }; + }); + }); }); }