Finish netplay create room logic (#163)
* Move files * more * auth * more
This commit is contained in:
parent
dbd12516cf
commit
9eacf8a916
@ -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]
|
||||
|
||||
@ -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())
|
||||
|
||||
204
src/ui/gui.rs
204
src/ui/gui.rs
@ -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
304
src/ui/gui/netplay.rs
Normal 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();
|
||||
};
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user