generated from tipragot/rust
Save
This commit is contained in:
parent
466265d451
commit
ca2433a40f
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -1278,8 +1278,10 @@ dependencies = [
|
|||
name = "border-wars"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bevnet",
|
||||
"bevy",
|
||||
"bevy_egui",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
@ -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>()),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -12,3 +12,5 @@ workspace = true
|
|||
[dependencies]
|
||||
bevy = "0.12.1"
|
||||
bevy_egui = "0.24.0"
|
||||
bevnet = { path = "../bevnet" }
|
||||
serde = "1.0.196"
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue