Save
Some checks failed
Rust Checks / checks (push) Failing after 2m41s
Rust Checks / checks (pull_request) Failing after 2m12s

This commit is contained in:
Tipragot 2024-02-10 16:58:49 +01:00
parent 466265d451
commit ca2433a40f
5 changed files with 341 additions and 43 deletions

2
Cargo.lock generated
View file

@ -1278,8 +1278,10 @@ dependencies = [
name = "border-wars"
version = "0.1.0"
dependencies = [
"bevnet",
"bevy",
"bevy_egui",
"serde",
]
[[package]]

View file

@ -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<TcpStream>,
/// Contains the buffers that are not yet being sent.
send_buffers: Mutex<LinkedList<(usize, Vec<u8>)>>,
@ -57,7 +59,7 @@ impl Connection {
fn new(stream: TcpStream, secret_key: &Key<Aes128Gcm>) -> io::Result<Self> {
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::<Aes128Gcm>::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<bool> {
// 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<LinkedList<Vec<u8>>> {
fn receive(&self, id: u16) -> io::Result<LinkedList<Vec<u8>>> {
// 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<Option<Connection>> {
fn accept(&self) -> io::Result<Option<Connection>> {
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<T: Event + Serialize + DeserializeOwned>(
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<T: Event + Serialize + DeserializeOwned>(pub T);
@ -511,11 +520,42 @@ pub struct SendAll<T: Event + Serialize + DeserializeOwned>(pub T);
impl<T: Event + Serialize + DeserializeOwned> SendAll<T> {
/// Returns a system that sends the events to all the connected clients on
/// the server.
pub fn send(event_id: u16) -> impl Fn(EventReader<Self>, Query<&Connection>) {
move |mut events: EventReader<Self>, connections: Query<&Connection>| {
pub fn send(
event_id: u16,
) -> impl Fn(EventReader<Self>, EventWriter<ServerEvent<T>>, Query<&Connection>) {
move |mut events: EventReader<Self>,
mut writer: EventWriter<ServerEvent<T>>,
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<T: Event + Serialize + DeserializeOwned>(pub Entity, pub T);
impl<T: Event + Serialize + DeserializeOwned> SendTo<T> {
/// Returns a system that sends the events to a specific client on the
/// server.
pub fn send(event_id: u16) -> impl Fn(EventReader<Self>, Query<&Connection>) {
move |mut events: EventReader<Self>, connections: Query<&Connection>| {
pub fn send(
event_id: u16,
) -> impl Fn(EventReader<Self>, EventWriter<ServerEvent<T>>, Query<&Connection>) {
move |mut events: EventReader<Self>,
mut writer: EventWriter<ServerEvent<T>>,
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<T: Event + Serialize + DeserializeOwned>(pub T);
impl<T: Event + Serialize + DeserializeOwned> SendToServer<T> {
/// Returns a system that sends the events from a client to the server.
pub fn send(event_id: u16) -> SystemConfigs {
(move |mut events: EventReader<Self>, connection: Option<Res<Connection>>| {
(move |mut events: EventReader<Self>,
mut writer: EventWriter<ClientEvent<T>>,
connection: Res<Connection>,
self_connections: Query<Entity, (With<Connection>, With<SelfConnection>)>| {
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::<Connection>();
}
}
/// A system that automatically create a new self [Connection] when a
/// [Listener] is created.
fn create_self_connection(mut commands: Commands, listener: Res<Listener>) {
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<Listener>) {
// 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::<Listener>()),
Self::update_client_connections,
Self::update_server_connection.run_if(resource_exists::<Connection>()),
Self::create_self_connection.run_if(resource_exists::<Listener>()),
),
);
}

View file

@ -12,3 +12,5 @@ workspace = true
[dependencies]
bevy = "0.12.1"
bevy_egui = "0.24.0"
bevnet = { path = "../bevnet" }
serde = "1.0.196"

View file

@ -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<String>,
mut message: Local<String>,
mut messages: Local<LinkedList<Message>>,
mut reader: EventReader<ServerEvent<Message>>,
mut writer: EventWriter<SendToServer<Message>>,
listener: Option<Res<Listener>>,
) {
// 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<ClientEvent<Message>>,
mut writer: EventWriter<SendAll<Message>>,
) {
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<Connection>, Without<SelfConnection>),
>,
) {
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<NextState<GameState>>,
connection: Option<Res<Connection>>,
) {
if connection.is_none() {
next_state.set(GameState::Menu);
}
}
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_state::<GameState>()
.add_plugins(MenuPlugin)
.add_plugins(NetworkPlugin)
.add_network_event::<Message>()
.add_systems(Update, chat_ui.run_if(resource_exists::<Connection>()))
.add_systems(Update, (handle_chat_events, return_to_menu))
.add_systems(Update, admin_ui.run_if(resource_exists::<Listener>()))
.run();
}

View file

@ -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<String>,
mut join_error_message: Local<Option<String>>,
mut host_error_message: Local<Option<String>>,
mut next_state: ResMut<NextState<GameState>>,
) {
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);
}
});
}