Finish netplay create room logic (#163)

* Move files

* more

* auth

* more
This commit is contained in:
Logan McNaughton 2025-01-19 22:35:56 +01:00 committed by GitHub
parent dbd12516cf
commit 9eacf8a916
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 330 additions and 181 deletions

View File

@ -22,6 +22,7 @@ sdl2-sys = "0.37"
spin_sleep = "1.3"
encoding_rs = "0.8"
mimalloc = "0.1"
tungstenite = "0.26"
clap = { version = "4.4", features = ["derive"] }
[dependencies.reqwest]

View File

@ -195,7 +195,7 @@ fn set_cic(device: &mut device::Device) {
}
}
fn calculate_hash(data: &[u8]) -> String {
pub fn calculate_hash(data: &[u8]) -> String {
let mut hasher = Sha256::new();
hasher.update(data);
format!("{:X}", hasher.finalize())

View File

@ -2,25 +2,13 @@ use crate::device;
use crate::ui;
use eframe::egui;
pub struct Netplay {
session_name: String,
password: String,
player_name: String,
server: (String, String),
servers: std::collections::HashMap<String, String>,
server_receiver: Option<tokio::sync::mpsc::Receiver<std::collections::HashMap<String, String>>>,
broadcast_socket: Option<std::net::UdpSocket>,
broadcast_timer: Option<std::time::Instant>,
}
mod netplay;
pub struct GopherEguiApp {
config_dir: std::path::PathBuf,
cache_dir: std::path::PathBuf,
data_dir: std::path::PathBuf,
configure_profile: bool,
netplay_create: bool,
netplay_join: bool,
netplay_wait: bool,
profile_name: String,
controllers: Vec<String>,
selected_controller: [i32; 4],
@ -36,7 +24,7 @@ pub struct GopherEguiApp {
vru_window_receiver: Option<tokio::sync::mpsc::Receiver<Vec<String>>>,
vru_word_notifier: Option<tokio::sync::mpsc::Sender<String>>,
vru_word_list: Vec<String>,
netplay: Netplay,
netplay: netplay::Netplay,
}
struct SaveConfig {
@ -105,9 +93,6 @@ impl GopherEguiApp {
config_dir: config_dir.clone(),
data_dir: data_dir.clone(),
configure_profile: false,
netplay_create: false,
netplay_join: false,
netplay_wait: false,
profile_name: "".to_string(),
selected_profile: game_ui.config.input.input_profile_binding.clone(),
selected_controller,
@ -123,10 +108,17 @@ impl GopherEguiApp {
vru_window_receiver: None,
vru_word_notifier: None,
vru_word_list: Vec::new(),
netplay: Netplay {
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(),
@ -177,162 +169,6 @@ impl Drop for GopherEguiApp {
}
}
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| {
let profile_name_label = ui.label("Profile Name:");
let mut size = ui.spacing().interact_size;
size.x = 200.0;
ui.add_sized(size, |ui: &mut egui::Ui| {
ui.text_edit_singleline(&mut app.netplay.session_name)
.labelled_by(profile_name_label.id)
});
ui.end_row();
let password_label = ui.label("Password (Optional):");
ui.text_edit_singleline(&mut app.netplay.password)
.labelled_by(password_label.id);
ui.end_row();
ui.label("ROM");
if ui.button("Open ROM").clicked() {
// Spawn dialog on main thread
let task = rfd::AsyncFileDialog::new().pick_file();
tokio::spawn(async {
let file = task.await;
if let Some(file) = file {
let _rom_contents = device::get_rom_contents(file.path());
}
});
}
ui.end_row();
let player_name_label = ui.label("Player Name:");
ui.text_edit_singleline(&mut app.netplay.player_name)
.labelled_by(player_name_label.id);
ui.end_row();
ui.label("Server:");
if app.netplay.servers.is_empty() {
if app.netplay.broadcast_socket.is_none() {
app.netplay.broadcast_socket = Some(
std::net::UdpSocket::bind((std::net::Ipv4Addr::UNSPECIFIED, 0))
.expect("couldn't bind to address"),
);
let socket = app.netplay.broadcast_socket.as_ref().unwrap();
socket
.set_broadcast(true)
.expect("set_broadcast call failed");
socket
.set_nonblocking(true)
.expect("could not set up socket");
let data: [u8; 1] = [1];
socket
.send_to(&data, (std::net::Ipv4Addr::BROADCAST, 45000))
.expect("couldn't send data");
app.netplay.broadcast_timer =
Some(std::time::Instant::now() + std::time::Duration::from_secs(5));
}
if app.netplay.server_receiver.is_none() {
let (tx, rx) = tokio::sync::mpsc::channel(1);
app.netplay.server_receiver = Some(rx);
tokio::spawn(async move {
if let Ok(response) =
reqwest::get("https://m64p.s3.amazonaws.com/servers.json").await
{
if let Ok(servers) = response
.json::<std::collections::HashMap<String, String>>()
.await
{
let _ = tx.send(servers).await;
}
}
});
}
}
if app.netplay.broadcast_timer.is_some()
&& std::time::Instant::now() > app.netplay.broadcast_timer.unwrap()
{
app.netplay.broadcast_timer = None;
}
if app.netplay.broadcast_socket.is_some() && app.netplay.broadcast_timer.is_some() {
let mut buffer = [0; 1024];
let result = app
.netplay
.broadcast_socket
.as_ref()
.unwrap()
.recv_from(&mut buffer);
if result.is_ok() {
let (amt, _src) = result.unwrap();
let data: std::collections::HashMap<String, String> =
serde_json::from_slice(&buffer[..amt]).unwrap();
for server in data.iter() {
let (server_name, server_ip) = server;
app.netplay
.servers
.insert(server_name.to_string(), server_ip.to_string());
app.netplay.server = (server.0.clone(), server.1.clone());
}
app.netplay.broadcast_socket = None;
}
}
if app.netplay.server_receiver.is_some() {
let result = app.netplay.server_receiver.as_mut().unwrap().try_recv();
if result.is_ok() {
app.netplay.servers.extend(result.unwrap());
app.netplay.server_receiver = None;
if app.netplay.server.0.is_empty() {
let first_server = app.netplay.servers.iter().next().unwrap();
app.netplay.server = (first_server.0.clone(), first_server.1.clone());
}
}
}
egui::ComboBox::from_id_salt("server-combobox")
.selected_text(app.netplay.server.0.to_string())
.show_ui(ui, |ui| {
for server in app.netplay.servers.iter() {
ui.selectable_value(
&mut app.netplay.server,
(server.0.clone(), server.1.clone()),
server.0,
);
}
});
ui.end_row();
if ui.button("Create Session").clicked() {
app.netplay_create = false;
app.netplay_wait = true;
}
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
if ui.button("Close").clicked() {
app.netplay_create = false
};
})
});
});
}
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
};
});
}
fn configure_profile(app: &mut GopherEguiApp, ctx: &egui::Context) {
egui::Window::new("Configure Input Profile")
// .open(&mut self.configure_profile)
@ -484,12 +320,20 @@ fn open_rom(app: &mut GopherEguiApp, ctx: &egui::Context) {
impl eframe::App for GopherEguiApp {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
if self.netplay_create {
netplay_create(self, ctx);
if self.netplay.create {
netplay::netplay_create(self, ctx);
}
if self.netplay_join {
netplay_join(self, ctx);
if self.netplay.join {
netplay::netplay_join(self, ctx);
}
if self.netplay.wait {
netplay::netplay_wait(self, ctx);
}
if !self.netplay.error.is_empty() {
netplay::netplay_error(self, ctx, self.netplay.error.clone());
}
if self.configure_profile {
@ -510,7 +354,7 @@ impl eframe::App for GopherEguiApp {
if ui.button("Netplay: Create Session").clicked()
&& !self.cache_dir.join("game_running").exists()
{
self.netplay_create = true;
self.netplay.create = true;
}
ui.end_row();
@ -524,7 +368,7 @@ impl eframe::App for GopherEguiApp {
if ui.button("Netplay: Join Session").clicked()
&& !self.cache_dir.join("game_running").exists()
{
self.netplay_join = true;
self.netplay.join = true;
}
});

304
src/ui/gui/netplay.rs Normal file
View File

@ -0,0 +1,304 @@
use crate::device;
use crate::ui::gui::GopherEguiApp;
use eframe::egui;
use sha2::{Digest, Sha256};
const NETPLAY_VERSION: i32 = 17;
const EMU_NAME: &str = "gopher64";
pub struct Netplay {
pub create: bool,
pub join: bool,
pub wait: bool,
pub session_name: String,
pub password: String,
pub player_name: String,
pub error: String,
pub rom_label: String,
pub server: (String, String),
pub game_info: (String, String, String),
pub servers: std::collections::HashMap<String, String>,
pub server_receiver:
Option<tokio::sync::mpsc::Receiver<std::collections::HashMap<String, String>>>,
pub game_info_receiver: Option<tokio::sync::mpsc::Receiver<(String, String, String)>>,
pub broadcast_socket: Option<std::net::UdpSocket>,
pub broadcast_timer: Option<std::time::Instant>,
}
#[derive(serde::Serialize, serde::Deserialize, std::fmt::Debug)]
pub struct NetplayRoom {
room_name: String,
password: Option<String>,
protected: Option<bool>,
#[serde(rename = "MD5")]
md5: String,
game_name: String,
port: Option<i32>,
}
#[derive(serde::Serialize, serde::Deserialize, std::fmt::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>,
#[serde(rename = "authTime")]
auth_time: Option<String>,
}
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| {
let profile_name_label = ui.label("Session Name:");
let mut size = ui.spacing().interact_size;
size.x = 200.0;
ui.add_sized(size, |ui: &mut egui::Ui| {
ui.text_edit_singleline(&mut app.netplay.session_name)
.labelled_by(profile_name_label.id)
});
ui.end_row();
let password_label = ui.label("Password (Optional):");
ui.text_edit_singleline(&mut app.netplay.password)
.labelled_by(password_label.id);
ui.end_row();
ui.label("ROM");
if ui.button(app.netplay.rom_label.clone()).clicked() {
let task = rfd::AsyncFileDialog::new().pick_file();
let (tx, rx) = tokio::sync::mpsc::channel(1);
app.netplay.game_info_receiver = Some(rx);
let gui_ctx = ctx.clone();
app.netplay.rom_label = "Inspecting ROM".to_string();
tokio::spawn(async move {
let file = task.await;
if let Some(file) = file {
let rom_contents = device::get_rom_contents(file.path());
if !rom_contents.is_empty() {
let hash = device::cart_rom::calculate_hash(&rom_contents);
let game_name = std::str::from_utf8(&rom_contents[0x20..0x20 + 0x14])
.unwrap()
.to_string();
let _ = tx.send((hash, game_name, file.file_name())).await;
gui_ctx.request_repaint();
} else {
let _ = tx
.send(("".to_string(), "".to_string(), "Invalid ROM".to_string()))
.await;
gui_ctx.request_repaint();
}
} else {
let _ = tx
.send(("".to_string(), "".to_string(), "Open ROM".to_string()))
.await;
gui_ctx.request_repaint();
}
});
}
ui.end_row();
let player_name_label = ui.label("Player Name:");
ui.text_edit_singleline(&mut app.netplay.player_name)
.labelled_by(player_name_label.id);
ui.end_row();
ui.label("Server:");
if app.netplay.servers.is_empty() {
if app.netplay.broadcast_socket.is_none() {
app.netplay.broadcast_socket = Some(
std::net::UdpSocket::bind((std::net::Ipv4Addr::UNSPECIFIED, 0))
.expect("couldn't bind to address"),
);
let socket = app.netplay.broadcast_socket.as_ref().unwrap();
socket
.set_broadcast(true)
.expect("set_broadcast call failed");
socket
.set_nonblocking(true)
.expect("could not set up socket");
let data: [u8; 1] = [1];
socket
.send_to(&data, (std::net::Ipv4Addr::BROADCAST, 45000))
.expect("couldn't send data");
app.netplay.broadcast_timer =
Some(std::time::Instant::now() + std::time::Duration::from_secs(5));
}
if app.netplay.server_receiver.is_none() {
let (tx, rx) = tokio::sync::mpsc::channel(1);
app.netplay.server_receiver = Some(rx);
let gui_ctx = ctx.clone();
tokio::spawn(async move {
if let Ok(response) =
reqwest::get("https://m64p.s3.amazonaws.com/servers.json").await
{
if let Ok(servers) = response
.json::<std::collections::HashMap<String, String>>()
.await
{
let _ = tx.send(servers).await;
gui_ctx.request_repaint();
}
}
});
}
}
if app.netplay.broadcast_timer.is_some()
&& std::time::Instant::now() > app.netplay.broadcast_timer.unwrap()
{
app.netplay.broadcast_timer = None;
}
if app.netplay.broadcast_socket.is_some() && app.netplay.broadcast_timer.is_some() {
let mut buffer = [0; 1024];
let result = app
.netplay
.broadcast_socket
.as_ref()
.unwrap()
.recv_from(&mut buffer);
if result.is_ok() {
let (amt, _src) = result.unwrap();
let data: std::collections::HashMap<String, String> =
serde_json::from_slice(&buffer[..amt]).unwrap();
for server in data.iter() {
let (server_name, server_ip) = server;
app.netplay
.servers
.insert(server_name.to_string(), server_ip.to_string());
app.netplay.server = (server.0.clone(), server.1.clone());
}
app.netplay.broadcast_socket = None;
}
}
if app.netplay.server_receiver.is_some() {
let result = app.netplay.server_receiver.as_mut().unwrap().try_recv();
if result.is_ok() {
app.netplay.servers.extend(result.unwrap());
app.netplay.server_receiver = None;
if app.netplay.server.0.is_empty() {
let first_server = app.netplay.servers.iter().next().unwrap();
app.netplay.server = (first_server.0.clone(), first_server.1.clone());
}
}
}
if app.netplay.game_info_receiver.is_some() {
let result = app.netplay.game_info_receiver.as_mut().unwrap().try_recv();
if result.is_ok() {
app.netplay.game_info_receiver = None;
let data = result.unwrap();
if !data.0.is_empty() {
app.netplay.game_info = data;
app.netplay.rom_label = app.netplay.game_info.2.clone();
} else {
app.netplay.rom_label = data.2;
}
}
}
egui::ComboBox::from_id_salt("server-combobox")
.selected_text(app.netplay.server.0.to_string())
.show_ui(ui, |ui| {
for server in app.netplay.servers.iter() {
ui.selectable_value(
&mut app.netplay.server,
(server.0.clone(), server.1.clone()),
server.0,
);
}
});
ui.end_row();
if ui.button("Create Session").clicked() {
if app.netplay.player_name.is_empty() {
app.netplay.error = "Player Name cannot be empty".to_string();
} else if app.netplay.session_name.is_empty() {
app.netplay.error = "Session Name cannot be empty".to_string();
} else if app.netplay.game_info.0.is_empty() {
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 netplay_message = NetplayMessage {
message_type: "request_create_room".to_string(),
player_name: Some(app.netplay.player_name.clone()),
client_sha: Some(env!("CARGO_PKG_VERSION").to_string()),
netplay_version: Some(NETPLAY_VERSION),
emulator: Some(EMU_NAME.to_string()),
accept: None,
message: None,
auth_time: Some(now_utc),
auth: Some(format!("{:x}", hasher.finalize())),
room: Some(NetplayRoom {
room_name: 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(),
protected: None,
port: None,
}),
};
let (mut socket, _response) =
tungstenite::connect(app.netplay.server.1.clone()).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();
}
}
}
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
if ui.button("Close").clicked() {
app.netplay.create = false
};
})
});
});
}
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
};
});
}
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
};
});
}
pub fn netplay_error(app: &mut GopherEguiApp, ctx: &egui::Context, error: String) {
egui::Window::new("Netplay Error").show(ctx, |ui| {
ui.label(error);
if ui.button("Close").clicked() {
app.netplay.error = "".to_string();
};
});
}