Implement generic online system (#82)
All checks were successful
Rust Checks / checks (push) Successful in 3m48s

note that the feature of adding a system to check if players are still connected will be in a new pull request.

Reviewed-on: fish-canard/border-wars#82
Reviewed-by: Raphaël <r.lauray@outlook.fr>
Co-authored-by: CoCo_Sol007 <solois.corentin@gmail.com>
Co-committed-by: CoCo_Sol007 <solois.corentin@gmail.com>
This commit is contained in:
CoCo_Sol 2024-03-25 05:45:14 +00:00 committed by CoCo_Sol
parent 04c376d5eb
commit 5848272f4a
9 changed files with 228 additions and 13 deletions

10
Cargo.lock generated
View file

@ -1314,10 +1314,12 @@ dependencies = [
name = "border-wars"
version = "0.1.0"
dependencies = [
"bevnet",
"bevy",
"bevy_egui",
"noise",
"paste",
"serde",
]
[[package]]
@ -3805,18 +3807,18 @@ dependencies = [
[[package]]
name = "serde"
version = "1.0.196"
version = "1.0.197"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32"
checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.196"
version = "1.0.197"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67"
checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b"
dependencies = [
"proc-macro2",
"quote",

View file

@ -15,3 +15,5 @@ bevy = "0.12.1"
bevy_egui = "0.24.0"
noise = "0.8.2"
paste = "1.0.14"
bevnet = { path = "../bevnet" }
serde = "1.0.197"

View file

@ -1,9 +1,13 @@
//! The file that contains utility functions, enums, structs for the game.
use bevnet::Uuid;
use bevy::prelude::*;
use networking::PlayerRank;
use serde::{Deserialize, Serialize};
pub mod camera;
pub mod map;
pub mod networking;
pub mod responsive_scale;
pub mod scenes;
@ -20,3 +24,16 @@ pub enum CurrentScene {
/// When we play this wonderful game.
Game,
}
/// A player in the game.
#[derive(Serialize, Deserialize, Clone, Debug, Component, Resource)]
pub struct Player {
/// The name of the player.
pub name: String,
/// The rank of the player.
pub rank: PlayerRank,
/// The uuid of the player.
pub uuid: Uuid,
}

View file

@ -5,6 +5,7 @@ use border_wars::camera::CameraPlugin;
use border_wars::map::generation::MapGenerationPlugin;
use border_wars::map::renderer::RendererPlugin;
use border_wars::map::selected_tile::SelectTilePlugin;
use border_wars::networking::NetworkingPlugin;
use border_wars::scenes::ScenesPlugin;
fn main() {
@ -14,6 +15,7 @@ fn main() {
.add_plugins(RendererPlugin)
.add_plugins(CameraPlugin)
.add_plugins(SelectTilePlugin)
.add_plugins(NetworkingPlugin)
.add_plugins(MapGenerationPlugin)
.run();
}

View file

@ -0,0 +1,95 @@
//! All the code related to the connection.
use bevnet::{Connection, NetworkAppExt, Receive, SendTo};
use bevy::prelude::*;
use serde::{Deserialize, Serialize};
use super::PlayerRank;
use crate::{CurrentScene, Player};
/// A plugin that manage connections (add, remove).
pub struct ConnectionPlugin;
impl Plugin for ConnectionPlugin {
fn build(&self, app: &mut App) {
app.add_network_event::<RequestJoin>()
.add_network_event::<AddPlayer>()
.add_network_event::<RemovePlayer>()
.add_systems(
Update,
(accept_connection, handle_new_player, handle_remove_player),
);
}
}
/// An event that is trigger when a new player request to join a game.
#[derive(Event, Serialize, Deserialize)]
pub struct RequestJoin(pub Player);
/// An event that is trigger when a new player is added.
#[derive(Event, Serialize, Deserialize)]
pub struct AddPlayer(Player);
/// An event that is trigger when a player is removed.
#[derive(Event, Serialize, Deserialize)]
pub struct RemovePlayer(pub Player);
/// A fonction that accept new connection.
/// It add the player to the list of all players.
pub fn accept_connection(
all_players_query: Query<&Player>,
mut requests_join_event: EventReader<Receive<RequestJoin>>,
mut add_players_event: EventWriter<SendTo<AddPlayer>>,
state: Res<State<CurrentScene>>,
) {
for request_join in requests_join_event.read() {
let mut new_player = request_join.1.0.clone();
let current_state = *state.get();
if current_state == CurrentScene::Menu {
return;
} else if current_state == CurrentScene::Game {
new_player.rank = PlayerRank::Spectator;
}
add_players_event.send(SendTo(new_player.uuid, AddPlayer(new_player.clone())));
for old_player in all_players_query.iter() {
// Link all players
add_players_event.send(SendTo(old_player.uuid, AddPlayer(new_player.clone())));
add_players_event.send(SendTo(new_player.uuid, AddPlayer(old_player.clone())));
}
}
}
/// A fonction that handle new players when a events is received.
pub fn handle_new_player(mut add_players: EventReader<Receive<AddPlayer>>, mut commands: Commands) {
for add_player in add_players.read() {
commands.spawn(add_player.1.0.clone());
}
}
/// A fonction that handle remove players when a events is received.
pub fn handle_remove_player(
mut remove_players: EventReader<Receive<RemovePlayer>>,
mut commands: Commands,
all_players_query: Query<(Entity, &Player)>,
connection: Res<Connection>,
mut next_scene: ResMut<NextState<CurrentScene>>,
) {
for remove_player in remove_players.read() {
if Some(remove_player.1.0.uuid) == connection.identifier() {
next_scene.set(CurrentScene::Menu);
all_players_query.iter().for_each(|(entity, _)| {
commands.entity(entity).despawn();
});
return;
}
for (entity, player) in all_players_query.iter() {
if remove_player.1.0.uuid == player.uuid {
commands.entity(entity).despawn();
}
}
}
}

View file

@ -0,0 +1,32 @@
//! All the code related to the networking.
use bevnet::NetworkPlugin;
use bevy::prelude::*;
use serde::{Deserialize, Serialize};
use self::connection::ConnectionPlugin;
pub mod connection;
/// The plugin for the networking.
pub struct NetworkingPlugin;
impl Plugin for NetworkingPlugin {
fn build(&self, app: &mut App) {
app.add_plugins(NetworkPlugin::new("relay.cocosol.fr".to_string()))
.add_plugins(ConnectionPlugin);
}
}
/// The rank of the player.
#[derive(PartialEq, Eq, Serialize, Deserialize, Clone, Copy, Debug)]
pub enum PlayerRank {
/// A spectator. He does not play the game, just renderer the game.
Spectator,
/// An admin. He manages the game and play the game.
Admin,
/// The player. He can join the game and play.
Player,
}

View file

@ -1,9 +1,12 @@
//! The lobby of the game.
use bevnet::{Connection, SendTo};
use bevy::prelude::*;
use bevy_egui::{egui, EguiContexts};
use crate::CurrentScene;
use crate::networking::connection::RemovePlayer;
use crate::networking::PlayerRank;
use crate::{CurrentScene, Player};
/// The plugin for the lobby.
pub struct LobbyPlugin;
@ -15,7 +18,21 @@ impl Plugin for LobbyPlugin {
}
/// Display the UI of the lobby.
fn lobby_ui(mut ctx: EguiContexts, mut next_scene: ResMut<NextState<CurrentScene>>) {
fn lobby_ui(
mut ctx: EguiContexts,
mut next_scene: ResMut<NextState<CurrentScene>>,
connection: Res<Connection>,
all_players_query: Query<&Player>,
mut kick_player: EventWriter<SendTo<RemovePlayer>>,
) {
// Get our player info.
let Some(self_player) = all_players_query
.iter()
.find(|player| connection.identifier() == Some(player.uuid))
else {
return;
};
egui::CentralPanel::default().show(ctx.ctx_mut(), |ui| {
ui.heading("Border Wars");
@ -23,14 +40,30 @@ fn lobby_ui(mut ctx: EguiContexts, mut next_scene: ResMut<NextState<CurrentScene
ui.label("Game created");
ui.horizontal(|ui| {
if self_player.rank != PlayerRank::Admin {
return;
}
ui.label("Game ID: ");
// TODO : get the game ID and display it.
ui.label("connection_string");
ui.text_edit_singleline(&mut connection.identifier().unwrap_or_default().to_string());
});
ui.separator();
if ui.button("Run the game").clicked() {
for player in all_players_query.iter() {
ui.label(player.name.to_string());
if self_player.rank == PlayerRank::Admin
&& player.rank != PlayerRank::Admin
&& ui.button("Remove").clicked()
{
for sender_id in all_players_query.iter() {
kick_player.send(SendTo(sender_id.uuid, RemovePlayer(player.clone())));
}
}
ui.separator();
}
if self_player.rank == PlayerRank::Admin && ui.button("Run the game").clicked() {
next_scene.set(CurrentScene::Game);
// TODO: run the game
}

View file

@ -1,9 +1,12 @@
//! The main menu of the game.
use bevnet::{Connection, SendTo, Uuid};
use bevy::prelude::*;
use bevy_egui::{egui, EguiContexts};
use crate::CurrentScene;
use crate::networking::connection::RequestJoin;
use crate::networking::PlayerRank;
use crate::{CurrentScene, Player};
/// The plugin for the menu.
pub struct MenuPlugin;
@ -13,25 +16,50 @@ impl Plugin for MenuPlugin {
app.add_systems(Update, menu_ui.run_if(in_state(CurrentScene::Menu)));
}
}
/// Display the UI of the menu to host a game or join one.
fn menu_ui(
mut ctx: EguiContexts,
mut connection_string: Local<String>,
mut next_scene: ResMut<NextState<CurrentScene>>,
mut request_join: EventWriter<SendTo<RequestJoin>>,
mut name: Local<String>,
connection: Res<Connection>,
mut commands: Commands,
) {
let Some(uuid) = connection.identifier() else {
return;
};
egui::CentralPanel::default().show(ctx.ctx_mut(), |ui| {
ui.heading("Border Wars");
ui.separator();
ui.label("Name");
ui.text_edit_singleline(&mut *name);
ui.separator();
ui.label("Connect to an existing game:");
ui.horizontal(|ui| {
ui.label("Game ID: ");
ui.text_edit_singleline(&mut *connection_string);
let Ok(game_id) = Uuid::parse_str(&connection_string) else {
return;
};
if ui.button("Join").clicked() {
next_scene.set(CurrentScene::Game);
// TODO: connect to the game
next_scene.set(CurrentScene::Lobby);
request_join.send(SendTo(
game_id,
RequestJoin(Player {
name: name.clone(),
rank: PlayerRank::Player,
uuid,
}),
));
}
});
@ -39,7 +67,11 @@ fn menu_ui(
if ui.button("Create new game").clicked() {
next_scene.set(CurrentScene::Lobby);
// TODO: create a new game
commands.spawn(Player {
name: name.clone(),
rank: PlayerRank::Admin,
uuid,
});
}
});
}

View file

@ -80,7 +80,7 @@ impl Connection {
path.push(".relay-data");
// Check if the file exists.
match path.exists() {
match false {
true => {
// Read the file and parse the identifier and secret key.
let contents = fs::read(&path)?;