From ca2433a40fd712070566886f371080568fc5008e Mon Sep 17 00:00:00 2001 From: Tipragot Date: Sat, 10 Feb 2024 16:58:49 +0100 Subject: [PATCH] Save --- Cargo.lock | 2 + crates/bevnet/src/lib.rs | 205 ++++++++++++++++++++++++++------- crates/border-wars/Cargo.toml | 2 + crates/border-wars/src/main.rs | 142 +++++++++++++++++++++++ crates/border-wars/src/menu.rs | 33 +++++- 5 files changed, 341 insertions(+), 43 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4a9258f..f38d404 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1278,8 +1278,10 @@ dependencies = [ name = "border-wars" version = "0.1.0" dependencies = [ + "bevnet", "bevy", "bevy_egui", + "serde", ] [[package]] diff --git a/crates/bevnet/src/lib.rs b/crates/bevnet/src/lib.rs index 75c85af..1bd77c5 100644 --- a/crates/bevnet/src/lib.rs +++ b/crates/bevnet/src/lib.rs @@ -23,7 +23,9 @@ use serde::Serialize; #[derive(Component, Resource)] pub struct Connection { /// The underlying [TcpStream] used for the connection. - stream: TcpStream, + /// + /// If `None`, the connection is self connected. + stream: Option, /// Contains the buffers that are not yet being sent. send_buffers: Mutex)>>, @@ -57,7 +59,7 @@ impl Connection { fn new(stream: TcpStream, secret_key: &Key) -> io::Result { stream.set_nonblocking(true)?; Ok(Self { - stream, + stream: Some(stream), send_buffers: Mutex::new(LinkedList::new()), received_messages: Mutex::new(HashMap::new()), receive_message_len: None, @@ -68,6 +70,20 @@ impl Connection { }) } + /// Creates a new [Connection] that connects to the current game. + fn new_self() -> Self { + Self { + stream: None, + send_buffers: Mutex::new(LinkedList::new()), + received_messages: Mutex::new(HashMap::new()), + receive_message_len: None, + receive_message_nonce: None, + receive_filled_len: 0, + receive_buffer: Vec::new(), + cipher: Aes128Gcm::new(&Key::::default()), + } + } + /// Creates a new [Connection] that connects to the given connection string. /// /// This function is blocking. @@ -99,7 +115,7 @@ impl Connection { /// Sends a message and its id over the connection. /// /// This function is not blocking. - pub fn send(&self, message: &[u8], id: u16) -> io::Result<()> { + fn send(&self, message: &[u8], id: u16) -> io::Result<()> { // Add the id to the message. let mut message_with_id = Vec::with_capacity(message.len() + 2); message_with_id.extend_from_slice(message); @@ -150,7 +166,12 @@ impl Connection { /// receives any pending messages that have not been received yet. /// /// This function is not blocking. - pub fn update(&mut self) -> io::Result<()> { + fn update(&mut self) -> io::Result<()> { + // Quit if the connection is self connected. + let Some(stream) = self.stream.as_mut() else { + return Ok(()); + }; + // Lock the send buffers. let send_buffers = self .send_buffers @@ -160,7 +181,7 @@ impl Connection { // Looping over the send buffers to send the messages. while let Some((offset, buffer)) = send_buffers.front_mut() { // Writing the buffer to the stream. - match self.stream.write(&buffer[*offset..]) { + match stream.write(&buffer[*offset..]) { Ok(n) => *offset += n, Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => break, Err(ref e) if e.kind() == io::ErrorKind::Interrupted => break, @@ -211,6 +232,11 @@ impl Connection { /// /// This function is not blocking. fn receive_partial(&mut self, len: u16) -> io::Result { + // Quit if the connection is self connected. + let Some(stream) = self.stream.as_mut() else { + return Ok(false); + }; + let len = len as usize; // Resizing the buffer if it is not large enough. @@ -227,7 +253,7 @@ impl Connection { // Reading from the stream. let start_index = self.receive_filled_len; let receive_buffer = &mut self.receive_buffer[start_index..start_index + len]; - let received_len = self.stream.read(receive_buffer); + let received_len = stream.read(receive_buffer); self.receive_filled_len += match received_len { Ok(0) => { return Err(io::Error::new( @@ -322,7 +348,7 @@ impl Connection { /// Receives all the messages with the given id from the connection. /// /// This function is not blocking. - pub fn receive(&self, id: u16) -> io::Result>> { + fn receive(&self, id: u16) -> io::Result>> { // Locking the received messages. let mut received_messages = self.received_messages.lock().map_err(|e| { io::Error::new( @@ -374,7 +400,7 @@ impl Listener { /// Accepts a new [Connection]. /// /// This function is not blocking. - pub fn accept(&self) -> io::Result> { + fn accept(&self) -> io::Result> { match self.0.accept() { Ok((stream, _)) => Connection::new(stream, &self.3).map(Some), Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => Ok(None), @@ -402,33 +428,16 @@ impl Drop for Listener { } } +/// A [Component] that marks an entity as containing a self [Connection]. +#[derive(Component)] +pub struct SelfConnection; + /// A resource that store the last event id used to register an [Event]. /// /// This is used to give an unique id to each event. #[derive(Resource, Default)] struct LastEventId(u16); -/// An utility function that sends an [Event] to a [Connection]. -fn send_event( - connection: &Connection, - event_id: u16, - event: &T, -) { - // Serializing the event. - let message = match bincode::serialize(event) { - Ok(message) => message, - Err(e) => { - error!("failed to serialize event: {e}"); - return; - } - }; - - // Sending the event. - if let Err(e) = connection.send(&message, event_id) { - info!("failed to send event: {e}"); - } -} - /// An [Event] received from the server on the client. #[derive(Event)] pub struct ServerEvent(pub T); @@ -511,11 +520,42 @@ pub struct SendAll(pub T); impl SendAll { /// Returns a system that sends the events to all the connected clients on /// the server. - pub fn send(event_id: u16) -> impl Fn(EventReader, Query<&Connection>) { - move |mut events: EventReader, connections: Query<&Connection>| { + pub fn send( + event_id: u16, + ) -> impl Fn(EventReader, EventWriter>, Query<&Connection>) { + move |mut events: EventReader, + mut writer: EventWriter>, + connections: Query<&Connection>| { for event in events.read() { + // Serializing the event. + let message = match bincode::serialize(&event.0) { + Ok(message) => message, + Err(e) => { + error!("failed to serialize event: {e}"); + return; + } + }; + for connection in connections.iter() { - send_event(connection, event_id, &event.0); + // If the connection is self connected. + if connection.stream.is_none() { + // Deserializing the message. + let event: T = match bincode::deserialize(&message) { + Ok(event) => event, + Err(e) => { + error!("failed to deserialize event: {e}"); + continue; + } + }; + + // Sending the event. + writer.send(ServerEvent(event)); + } else { + // Sending the event through the connection. + if let Err(e) = connection.send(&message, event_id) { + error!("failed to send event: {e}"); + } + } } } } @@ -529,11 +569,44 @@ pub struct SendTo(pub Entity, pub T); impl SendTo { /// Returns a system that sends the events to a specific client on the /// server. - pub fn send(event_id: u16) -> impl Fn(EventReader, Query<&Connection>) { - move |mut events: EventReader, connections: Query<&Connection>| { + pub fn send( + event_id: u16, + ) -> impl Fn(EventReader, EventWriter>, Query<&Connection>) { + move |mut events: EventReader, + mut writer: EventWriter>, + connections: Query<&Connection>| { for event in events.read() { + // Serializing the event. + let message = match bincode::serialize(&event.0) { + Ok(message) => message, + Err(e) => { + error!("failed to serialize event: {e}"); + return; + } + }; + match connections.get(event.0) { - Ok(connection) => send_event(connection, event_id, &event.1), + Ok(connection) => { + // If the connection is self connected. + if connection.stream.is_none() { + // Deserializing the message. + let event: T = match bincode::deserialize(&message) { + Ok(event) => event, + Err(e) => { + error!("failed to deserialize event: {e}"); + continue; + } + }; + + // Sending the event. + writer.send(ServerEvent(event)); + } else { + // Sending the event through the connection. + if let Err(e) = connection.send(&message, event_id) { + error!("failed to send event: {e}"); + } + } + } Err(e) => error!("tried to send event to non-existent connection: {e}"), } } @@ -548,12 +621,38 @@ pub struct SendToServer(pub T); impl SendToServer { /// Returns a system that sends the events from a client to the server. pub fn send(event_id: u16) -> SystemConfigs { - (move |mut events: EventReader, connection: Option>| { + (move |mut events: EventReader, + mut writer: EventWriter>, + connection: Res, + self_connections: Query, With)>| { for event in events.read() { - if let Some(connection) = connection.as_ref() { - send_event(connection, event_id, &event.0); + // Serializing the event. + let message = match bincode::serialize(&event.0) { + Ok(message) => message, + Err(e) => { + error!("failed to serialize event: {e}"); + return; + } + }; + + // If the connection is self connected. + if connection.stream.is_none() { + // Deserializing the message. + let event: T = match bincode::deserialize(&message) { + Ok(event) => event, + Err(e) => { + error!("failed to deserialize event: {e}"); + continue; + } + }; + + // Sending the event to the self connection. + writer.send(ClientEvent(self_connections.single(), event)); } else { - error!("tried to send event to non-existent connection"); + // Sending the event through the connection. + if let Err(e) = connection.send(&message, event_id) { + error!("failed to send event: {e}"); + } } } }) @@ -585,6 +684,32 @@ impl NetworkPlugin { commands.remove_resource::(); } } + + /// A system that automatically create a new self [Connection] when a + /// [Listener] is created. + fn create_self_connection(mut commands: Commands, listener: Res) { + if listener.is_changed() { + commands.insert_resource(Connection::new_self()); + commands.spawn((Connection::new_self(), SelfConnection)); + } + } + + /// A system that accepts new [Connection] from the [Listener] of the + /// server. + fn accept_new_connections(mut commands: Commands, listener: Res) { + // Accept new connections. + let connection = match listener.accept() { + Ok(Some(connection)) => connection, + Ok(None) => return, + Err(e) => { + error!("failed to accept new connection: {e}"); + return; + } + }; + + // Insert the new connection into the world. + commands.spawn(connection); + } } impl Plugin for NetworkPlugin { @@ -592,8 +717,10 @@ impl Plugin for NetworkPlugin { app.add_systems( Last, ( + Self::accept_new_connections.run_if(resource_exists::()), Self::update_client_connections, Self::update_server_connection.run_if(resource_exists::()), + Self::create_self_connection.run_if(resource_exists::()), ), ); } diff --git a/crates/border-wars/Cargo.toml b/crates/border-wars/Cargo.toml index 1c977be..c2e04e3 100644 --- a/crates/border-wars/Cargo.toml +++ b/crates/border-wars/Cargo.toml @@ -12,3 +12,5 @@ workspace = true [dependencies] bevy = "0.12.1" bevy_egui = "0.24.0" +bevnet = { path = "../bevnet" } +serde = "1.0.196" diff --git a/crates/border-wars/src/main.rs b/crates/border-wars/src/main.rs index ec04b20..4aa26e8 100644 --- a/crates/border-wars/src/main.rs +++ b/crates/border-wars/src/main.rs @@ -1,13 +1,155 @@ //! The main entry point of the game. +#![windows_subsystem = "windows"] + +use std::collections::LinkedList; + +use bevnet::{ + ClientEvent, Connection, Listener, NetworkAppExt, NetworkPlugin, SelfConnection, SendAll, + SendToServer, ServerEvent, +}; use bevy::prelude::*; +use bevy_egui::egui::{self, Key}; +use bevy_egui::EguiContexts; use border_wars::menu::MenuPlugin; use border_wars::GameState; +use serde::{Deserialize, Serialize}; + +/// A message that can be sent between clients. +#[derive(Event, Serialize, Deserialize, Clone)] +struct Message { + /// The username of the sender. + username: String, + + /// The content of the message. + content: String, +} + +/// A system that display a chat window. +fn chat_ui( + mut ctx: EguiContexts, + mut username: Local, + mut message: Local, + mut messages: Local>, + mut reader: EventReader>, + mut writer: EventWriter>, + listener: Option>, +) { + // Read all new messages from the server. + for event in reader.read() { + messages.push_back(event.0.clone()); + } + + // Display the chat window. + egui::Window::new("Chat").show(ctx.ctx_mut(), |ui| { + if let Some(listener) = listener.as_ref() { + ui.horizontal(|ui| { + ui.label("Game ID: "); + }); + ui.text_edit_singleline(&mut listener.connection_string()); + } + + ui.horizontal(|ui| { + ui.label("Username:"); + ui.text_edit_singleline(&mut *username); + }); + + for message in messages.iter() { + ui.label(format!("{}: {}", message.username, message.content)); + } + + ui.horizontal(|ui| { + ui.label("Message:"); + let text = ui.text_edit_singleline(&mut *message); + + if (text.lost_focus() && text.ctx.input(|i| i.key_pressed(Key::Enter))) + || ui.button("Send").clicked() + { + let username = username.trim(); + if username.is_empty() { + return; + } + let trimmed_message = message.trim(); + if trimmed_message.is_empty() { + return; + } + writer.send(SendToServer(Message { + username: username.to_owned(), + content: trimmed_message.to_owned(), + })); + message.clear(); + } + }); + }); +} + +/// A system that handles chat events from the clients. +fn handle_chat_events( + mut commands: Commands, + mut reader: EventReader>, + mut writer: EventWriter>, +) { + for event in reader.read() { + info!("{} sent: {}", event.1.username, event.1.content); + commands + .entity(event.0) + .insert(LastUsername(event.1.username.clone())); + writer.send(SendAll(event.1.clone())); + } +} + +/// A [Component] that stores the last username used by a client. +#[derive(Component)] +struct LastUsername(String); + +/// A system that display the admin window for the server. +fn admin_ui( + mut ctx: EguiContexts, + mut commands: Commands, + connections: Query< + (Entity, Option<&LastUsername>), + (With, Without), + >, +) { + egui::Window::new("Admin").show(ctx.ctx_mut(), |ui| { + ui.label("Connected clients:"); + for (entity, last_username) in connections.iter() { + let name = last_username + .map(|u| u.0.clone()) + .unwrap_or(format!("{entity:?}")); + ui.horizontal(|ui| { + ui.label(name); + if ui + .button("Kick") + .on_hover_text("Kicks the client") + .clicked() + { + commands.entity(entity).despawn(); + } + }); + } + }); +} + +/// A system to return to the menu if we got disconnected. +fn return_to_menu( + mut next_state: ResMut>, + connection: Option>, +) { + if connection.is_none() { + next_state.set(GameState::Menu); + } +} fn main() { App::new() .add_plugins(DefaultPlugins) .add_state::() .add_plugins(MenuPlugin) + .add_plugins(NetworkPlugin) + .add_network_event::() + .add_systems(Update, chat_ui.run_if(resource_exists::())) + .add_systems(Update, (handle_chat_events, return_to_menu)) + .add_systems(Update, admin_ui.run_if(resource_exists::())) .run(); } diff --git a/crates/border-wars/src/menu.rs b/crates/border-wars/src/menu.rs index d6e4d13..d5d71da 100644 --- a/crates/border-wars/src/menu.rs +++ b/crates/border-wars/src/menu.rs @@ -1,5 +1,6 @@ //! The main menu of the game. +use bevnet::{Connection, Listener}; use bevy::prelude::*; use bevy_egui::{egui, EguiContexts, EguiPlugin}; @@ -18,8 +19,11 @@ impl Plugin for MenuPlugin { } /// Display the UI of the menu to host a game or join one. fn menu_ui( + mut commands: Commands, mut ctx: EguiContexts, mut connection_string: Local, + mut join_error_message: Local>, + mut host_error_message: Local>, mut next_state: ResMut>, ) { egui::CentralPanel::default().show(ctx.ctx_mut(), |ui| { @@ -33,16 +37,37 @@ fn menu_ui( ui.text_edit_singleline(&mut *connection_string); if ui.button("Join").clicked() { - next_state.set(GameState::Game); - // TODO: connect to the game + match Connection::connect(&connection_string) { + Ok(connection) => { + commands.insert_resource(connection); + next_state.set(GameState::Game); + } + Err(e) => { + *join_error_message = Some(format!("Failed to connect: {}", e)); + } + }; } }); + if let Some(message) = join_error_message.as_ref() { + ui.label(message); + } ui.separator(); if ui.button("Create new game").clicked() { - next_state.set(GameState::Lobby); - // TODO: create a new game + match Listener::new() { + Ok(listener) => { + info!("Game ID: {}", listener.connection_string()); + commands.insert_resource(listener); + next_state.set(GameState::Lobby); + } + Err(e) => { + *host_error_message = Some(format!("Failed to start server: {}", e)); + } + } + } + if let Some(message) = host_error_message.as_ref() { + ui.label(message); } }); }