Compare commits
No commits in common. "new-network" and "main" have entirely different histories.
new-networ
...
main
2
.cargo/config.toml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
[build]
|
||||||
|
rustflags = ["-Z", "threads=8"]
|
4285
Cargo.lock
generated
20
crates/bevnet/Cargo.toml
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
[package]
|
||||||
|
name = "bevnet"
|
||||||
|
version = "0.2.0"
|
||||||
|
edition = "2021"
|
||||||
|
license = "GPL-3.0-or-later"
|
||||||
|
description = "A library for networking in Bevy."
|
||||||
|
authors = ["Tipragot <contact@tipragot.fr>"]
|
||||||
|
keywords = ["bevy", "network", "game"]
|
||||||
|
categories = ["network-programming", "game-development"]
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
relay-client = { path = "../relay-client" }
|
||||||
|
serde = "1.0.196"
|
||||||
|
bincode = "1.3.3"
|
||||||
|
dashmap = "5.5.3"
|
||||||
|
bevy = "0.12.1"
|
||||||
|
uuid = "1.7.0"
|
152
crates/bevnet/src/lib.rs
Normal file
|
@ -0,0 +1,152 @@
|
||||||
|
//! A networking library for Bevy.
|
||||||
|
|
||||||
|
use std::borrow::Cow;
|
||||||
|
use std::collections::LinkedList;
|
||||||
|
|
||||||
|
use bevy::prelude::*;
|
||||||
|
use dashmap::DashMap;
|
||||||
|
use serde::de::DeserializeOwned;
|
||||||
|
use serde::Serialize;
|
||||||
|
pub use uuid::Uuid;
|
||||||
|
|
||||||
|
/// A connection to a relay server.
|
||||||
|
#[derive(Resource)]
|
||||||
|
pub struct Connection(relay_client::Connection);
|
||||||
|
|
||||||
|
/// A resource that stores the received messages.
|
||||||
|
#[derive(Resource)]
|
||||||
|
pub struct ReceivedMessages(DashMap<u16, LinkedList<(Uuid, Vec<u8>)>>);
|
||||||
|
|
||||||
|
impl Connection {
|
||||||
|
/// Returns the identifier of the connection.
|
||||||
|
pub const fn identifier(&self) -> Option<Uuid> {
|
||||||
|
self.0.identifier()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A bevy plugin to make multiplayer game using a relay server.
|
||||||
|
pub struct NetworkPlugin(String);
|
||||||
|
|
||||||
|
impl NetworkPlugin {
|
||||||
|
/// Create a new [NetworkPlugin] plugin with the given domain for the relay
|
||||||
|
/// server.
|
||||||
|
pub fn new<'a>(domain: impl Into<Cow<'a, str>>) -> Self {
|
||||||
|
Self(domain.into().into_owned())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update the relay connection.
|
||||||
|
fn update_connection(mut connection: ResMut<Connection>, received_messages: Res<ReceivedMessages>) {
|
||||||
|
let messages = connection.0.update();
|
||||||
|
for (sender, mut message) in messages {
|
||||||
|
if message.len() < 2 {
|
||||||
|
error!("message too short received");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let id_start = message.len() - 2;
|
||||||
|
let event_id = u16::from_be_bytes([message[id_start], message[id_start + 1]]);
|
||||||
|
message.truncate(id_start);
|
||||||
|
received_messages
|
||||||
|
.0
|
||||||
|
.entry(event_id)
|
||||||
|
.or_default()
|
||||||
|
.push_back((sender, message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A system that clear the received messages.
|
||||||
|
fn clear_received_messages(received_messages: Res<ReceivedMessages>) {
|
||||||
|
received_messages.0.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Plugin for NetworkPlugin {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
app.insert_resource(Connection(
|
||||||
|
relay_client::Connection::new(&self.0).expect("could not create connection"),
|
||||||
|
))
|
||||||
|
.insert_resource(ReceivedMessages(DashMap::new()))
|
||||||
|
.add_systems(PreUpdate, update_connection)
|
||||||
|
.add_systems(PreUpdate, clear_received_messages.after(update_connection));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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 [Event] used to send an [Event] to another client on the relay server.
|
||||||
|
#[derive(Event)]
|
||||||
|
pub struct SendTo<T: Event + DeserializeOwned + Serialize>(pub Uuid, pub T);
|
||||||
|
|
||||||
|
/// An [Event] used to receive an [Event] from another client on the relay
|
||||||
|
/// server.
|
||||||
|
#[derive(Event)]
|
||||||
|
pub struct Receive<T: Event + DeserializeOwned + Serialize>(pub Uuid, pub T);
|
||||||
|
|
||||||
|
/// A trait that extends a bevy [App] to add multiplayer support.
|
||||||
|
pub trait NetworkAppExt {
|
||||||
|
/// Setup the application to manage network events of type `T`.
|
||||||
|
fn add_network_event<T: Event + DeserializeOwned + Serialize>(&mut self) -> &mut Self;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NetworkAppExt for App {
|
||||||
|
fn add_network_event<T: Event + DeserializeOwned + Serialize>(&mut self) -> &mut Self {
|
||||||
|
// Get a new event id.
|
||||||
|
let mut event_id = self.world.get_resource_or_insert_with(LastEventId::default);
|
||||||
|
event_id.0 += 1;
|
||||||
|
let event_id = event_id.0;
|
||||||
|
|
||||||
|
// Register the event.
|
||||||
|
self.add_event::<SendTo<T>>()
|
||||||
|
.add_event::<Receive<T>>()
|
||||||
|
.add_systems(
|
||||||
|
PreUpdate,
|
||||||
|
(move |mut events: EventReader<SendTo<T>>, connection: Res<Connection>| {
|
||||||
|
for event in events.read() {
|
||||||
|
// Get the size of the serialized event.
|
||||||
|
let size = match bincode::serialized_size(&event.1) {
|
||||||
|
Ok(size) => size,
|
||||||
|
Err(e) => {
|
||||||
|
error!("failed to serialize event: {}", e);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Serialize the event we add 18 here because we will add the event id (2
|
||||||
|
// bytes) at the end and after that, the relay client will add the target id
|
||||||
|
// at the end (16 bytes).
|
||||||
|
let mut data = Vec::with_capacity(size as usize + 18);
|
||||||
|
if let Err(e) = bincode::serialize_into(&mut data, &event.1) {
|
||||||
|
error!("failed to serialize event: {}", e);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the event id.
|
||||||
|
data.extend_from_slice(&event_id.to_be_bytes());
|
||||||
|
|
||||||
|
// Send the event.
|
||||||
|
connection.0.send(event.0, data);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.before(update_connection),
|
||||||
|
)
|
||||||
|
.add_systems(
|
||||||
|
PreUpdate,
|
||||||
|
(move |mut writer: EventWriter<Receive<T>>,
|
||||||
|
received_messages: Res<ReceivedMessages>| {
|
||||||
|
if let Some(mut messages) = received_messages.0.get_mut(&event_id) {
|
||||||
|
while let Some((sender, message)) = messages.pop_front() {
|
||||||
|
match bincode::deserialize(&message) {
|
||||||
|
Ok(event) => writer.send(Receive(sender, event)),
|
||||||
|
Err(e) => error!("failed to deserialize event: {}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.before(clear_received_messages)
|
||||||
|
.after(update_connection),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
20
crates/border-wars/Cargo.toml
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
[package]
|
||||||
|
name = "border-wars"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
license = "GPL-3.0-or-later"
|
||||||
|
description = "An online turn based game."
|
||||||
|
repository = "https://git.tipragot.fr/corentin/border-wars.git"
|
||||||
|
authors = ["CoCoSol"]
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
bevy = "0.12.1"
|
||||||
|
bevy_egui = "0.24.0"
|
||||||
|
noise = "0.8.2"
|
||||||
|
paste = "1.0.14"
|
||||||
|
bevnet = { path = "../bevnet" }
|
||||||
|
serde = "1.0.197"
|
||||||
|
rand = "0.8.5"
|
28
crates/border-wars/assets/tiles/License.txt
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
|
||||||
|
|
||||||
|
Hexagon Kit (2.0)
|
||||||
|
|
||||||
|
Created/distributed by Kenney (www.kenney.nl)
|
||||||
|
Creation date: 23-01-2024 11:58
|
||||||
|
|
||||||
|
------------------------------
|
||||||
|
|
||||||
|
License: (Creative Commons Zero, CC0)
|
||||||
|
http://creativecommons.org/publicdomain/zero/1.0/
|
||||||
|
|
||||||
|
You can use this content for personal, educational, and commercial purposes.
|
||||||
|
|
||||||
|
Support by crediting 'Kenney' or 'www.kenney.nl' (this is not a requirement)
|
||||||
|
|
||||||
|
------------------------------
|
||||||
|
|
||||||
|
• Website : www.kenney.nl
|
||||||
|
• Donate : www.kenney.nl/donate
|
||||||
|
|
||||||
|
• Patreon : patreon.com/kenney
|
||||||
|
|
||||||
|
Follow on social media for updates:
|
||||||
|
|
||||||
|
• Twitter: twitter.com/KenneyNL
|
||||||
|
• Instagram: instagram.com/kenney_nl
|
||||||
|
• Mastodon: mastodon.gamedev.place/@kenney
|
BIN
crates/border-wars/assets/tiles/breeding.png
Normal file
After Width: | Height: | Size: 23 KiB |
BIN
crates/border-wars/assets/tiles/casern.png
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
crates/border-wars/assets/tiles/castle.png
Normal file
After Width: | Height: | Size: 34 KiB |
BIN
crates/border-wars/assets/tiles/forest.png
Normal file
After Width: | Height: | Size: 24 KiB |
BIN
crates/border-wars/assets/tiles/grass.png
Normal file
After Width: | Height: | Size: 6.8 KiB |
BIN
crates/border-wars/assets/tiles/hill.png
Normal file
After Width: | Height: | Size: 24 KiB |
BIN
crates/border-wars/assets/tiles/mine.png
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
crates/border-wars/assets/tiles/outpost.png
Normal file
After Width: | Height: | Size: 21 KiB |
BIN
crates/border-wars/assets/tiles/sawmill.png
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
crates/border-wars/assets/tiles/tower.png
Normal file
After Width: | Height: | Size: 23 KiB |
BIN
crates/border-wars/assets/tiles/wall.png
Normal file
After Width: | Height: | Size: 26 KiB |
159
crates/border-wars/src/camera.rs
Normal file
|
@ -0,0 +1,159 @@
|
||||||
|
//! This module contains the camera systems responsible for movement and
|
||||||
|
//! scaling.
|
||||||
|
|
||||||
|
use bevy::input::mouse::{MouseScrollUnit, MouseWheel};
|
||||||
|
use bevy::prelude::*;
|
||||||
|
|
||||||
|
use crate::CurrentScene;
|
||||||
|
|
||||||
|
/// The speed of camera movement.
|
||||||
|
#[derive(Resource)]
|
||||||
|
struct CameraSpeedMouvement(f32);
|
||||||
|
|
||||||
|
/// The speed of camera scaling.
|
||||||
|
#[derive(Resource)]
|
||||||
|
struct CameraSpeedScale(f32);
|
||||||
|
|
||||||
|
/// The minimum scale of the camera.
|
||||||
|
#[derive(Resource)]
|
||||||
|
struct MinimumScale(f32);
|
||||||
|
|
||||||
|
/// The maximum scale of the camera.
|
||||||
|
#[derive(Resource)]
|
||||||
|
struct MaximumScale(f32);
|
||||||
|
|
||||||
|
/// Key settings for camera movement.
|
||||||
|
#[derive(Resource)]
|
||||||
|
pub struct KeysMovementSettings {
|
||||||
|
/// Key to move the camera up.
|
||||||
|
pub up: KeyCode,
|
||||||
|
|
||||||
|
/// Key to move the camera down.
|
||||||
|
pub down: KeyCode,
|
||||||
|
|
||||||
|
/// Key to move the camera right.
|
||||||
|
pub right: KeyCode,
|
||||||
|
|
||||||
|
/// Key to move the camera left.
|
||||||
|
pub left: KeyCode,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A Bevy plugin for the camera.
|
||||||
|
/// Allows camera movement with the keyboard and scaling with the mouse.
|
||||||
|
pub struct CameraPlugin;
|
||||||
|
|
||||||
|
impl Plugin for CameraPlugin {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
app.add_systems(Startup, init_camera)
|
||||||
|
.add_systems(Startup, init_resources_for_camera)
|
||||||
|
.add_systems(
|
||||||
|
Update,
|
||||||
|
(keyboard_movement_system, mouse_movement_system)
|
||||||
|
.run_if(in_state(CurrentScene::Game)),
|
||||||
|
)
|
||||||
|
.add_systems(Update, scale_system.run_if(in_state(CurrentScene::Game)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Initializes the camera.
|
||||||
|
fn init_camera(mut commands: Commands) {
|
||||||
|
commands.spawn(Camera2dBundle::default());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Initializes the resources related to the camera.
|
||||||
|
///
|
||||||
|
/// - [KeysMovementSettings]: The key settings for camera movement.
|
||||||
|
/// - [CameraSpeedMouvement]: The speed of camera movement.
|
||||||
|
/// - [CameraSpeedScale]: The speed of camera scaling.
|
||||||
|
/// - [MinimumScale]: The minimum scale of the camera.
|
||||||
|
/// - [MaximumScale]: The maximum scale of the camera.
|
||||||
|
fn init_resources_for_camera(mut commands: Commands) {
|
||||||
|
commands.insert_resource(KeysMovementSettings {
|
||||||
|
up: KeyCode::Z,
|
||||||
|
down: KeyCode::S,
|
||||||
|
right: KeyCode::D,
|
||||||
|
left: KeyCode::Q,
|
||||||
|
});
|
||||||
|
|
||||||
|
commands.insert_resource(CameraSpeedMouvement(400.0));
|
||||||
|
commands.insert_resource(CameraSpeedScale(0.1));
|
||||||
|
commands.insert_resource(MinimumScale(0.1));
|
||||||
|
commands.insert_resource(MaximumScale(10.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Moves the camera with keyboard input.
|
||||||
|
fn keyboard_movement_system(
|
||||||
|
mut query: Query<&mut Transform, With<Camera>>,
|
||||||
|
keys: Res<Input<KeyCode>>,
|
||||||
|
keys_settings: Res<KeysMovementSettings>,
|
||||||
|
movement_speed: Res<CameraSpeedMouvement>,
|
||||||
|
delta_time: Res<Time>,
|
||||||
|
) {
|
||||||
|
for mut transform in query.iter_mut() {
|
||||||
|
let mut dx = 0.0;
|
||||||
|
let mut dy = 0.0;
|
||||||
|
for key in keys.get_pressed() {
|
||||||
|
match *key {
|
||||||
|
up if up == keys_settings.up => dy += movement_speed.0,
|
||||||
|
down if down == keys_settings.down => dy -= movement_speed.0,
|
||||||
|
right if right == keys_settings.right => dx += movement_speed.0,
|
||||||
|
left if left == keys_settings.left => dx -= movement_speed.0,
|
||||||
|
_ => continue,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
transform.translation.x += dx * delta_time.delta_seconds();
|
||||||
|
transform.translation.y += dy * delta_time.delta_seconds();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Moves the camera with mouse input.
|
||||||
|
fn mouse_movement_system(
|
||||||
|
mouse_button_input: Res<Input<MouseButton>>,
|
||||||
|
mut query: Query<(&mut Transform, &OrthographicProjection), With<Camera>>,
|
||||||
|
windows: Query<&Window>,
|
||||||
|
mut last_position: Local<Option<Vec2>>,
|
||||||
|
) {
|
||||||
|
let window = windows.get_single().expect("Main window not found");
|
||||||
|
let Some(position) = window.cursor_position() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if mouse_button_input.just_pressed(MouseButton::Right) {
|
||||||
|
*last_position = Some(position);
|
||||||
|
}
|
||||||
|
|
||||||
|
if mouse_button_input.just_released(MouseButton::Right) {
|
||||||
|
*last_position = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(old_position) = *last_position {
|
||||||
|
for (mut transform, projection) in query.iter_mut() {
|
||||||
|
let offset = (old_position - position).extend(0.0) * Vec3::new(1., -1., 1.);
|
||||||
|
transform.translation += offset * projection.scale;
|
||||||
|
}
|
||||||
|
*last_position = Some(position);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Scales the view with mouse input.
|
||||||
|
fn scale_system(
|
||||||
|
mut scroll_event: EventReader<MouseWheel>,
|
||||||
|
mut query: Query<&mut OrthographicProjection, With<Camera>>,
|
||||||
|
min_scale: Res<MinimumScale>,
|
||||||
|
max_scale: Res<MaximumScale>,
|
||||||
|
scale_speed: Res<CameraSpeedScale>,
|
||||||
|
) {
|
||||||
|
for event in scroll_event.read() {
|
||||||
|
for mut projection in query.iter_mut() {
|
||||||
|
if event.unit != MouseScrollUnit::Line {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let future_scale = event.y.mul_add(-scale_speed.0, projection.scale);
|
||||||
|
if min_scale.0 < future_scale && future_scale < max_scale.0 {
|
||||||
|
projection.scale = future_scale;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
46
crates/border-wars/src/lib.rs
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
//! 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 resources;
|
||||||
|
pub mod scenes;
|
||||||
|
pub mod ui;
|
||||||
|
|
||||||
|
/// A scene of the game.
|
||||||
|
#[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Hash, States)]
|
||||||
|
pub enum Scene {
|
||||||
|
/// When we are in the main menu.
|
||||||
|
#[default]
|
||||||
|
Menu,
|
||||||
|
|
||||||
|
/// When we are in the lobby waiting for players to join the game.
|
||||||
|
Lobby,
|
||||||
|
|
||||||
|
/// When we play this wonderful game.
|
||||||
|
Game,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The current scene of the game.
|
||||||
|
pub type CurrentScene = Scene;
|
||||||
|
|
||||||
|
/// A player in the game.
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug, Component, Resource, PartialEq, Eq, Hash)]
|
||||||
|
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,
|
||||||
|
|
||||||
|
/// The color of the player.
|
||||||
|
pub color: (u8, u8, u8),
|
||||||
|
}
|
27
crates/border-wars/src/main.rs
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
//! The main entry point of the game.
|
||||||
|
|
||||||
|
use bevy::prelude::*;
|
||||||
|
use border_wars::camera::CameraPlugin;
|
||||||
|
use border_wars::map::generation::MapGenerationPlugin;
|
||||||
|
use border_wars::map::ownership::OwnershipPlugin;
|
||||||
|
use border_wars::map::renderer::RendererPlugin;
|
||||||
|
use border_wars::map::selected_tile::SelectTilePlugin;
|
||||||
|
use border_wars::networking::NetworkingPlugin;
|
||||||
|
use border_wars::resources::ResourcesPlugin;
|
||||||
|
use border_wars::scenes::ScenesPlugin;
|
||||||
|
use border_wars::ui::UiPlugin;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
App::new()
|
||||||
|
.add_plugins(DefaultPlugins)
|
||||||
|
.add_plugins(ScenesPlugin)
|
||||||
|
.add_plugins(RendererPlugin)
|
||||||
|
.add_plugins(CameraPlugin)
|
||||||
|
.add_plugins(SelectTilePlugin)
|
||||||
|
.add_plugins(NetworkingPlugin)
|
||||||
|
.add_plugins(MapGenerationPlugin)
|
||||||
|
.add_plugins(UiPlugin)
|
||||||
|
.add_plugins(OwnershipPlugin)
|
||||||
|
.add_plugins(ResourcesPlugin)
|
||||||
|
.run();
|
||||||
|
}
|
99
crates/border-wars/src/map/generation.rs
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
//! All functions related to the generation of the map.
|
||||||
|
|
||||||
|
use bevy::prelude::*;
|
||||||
|
use noise::{NoiseFn, Perlin};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use super::hex::*;
|
||||||
|
use super::{Tile, TilePosition};
|
||||||
|
|
||||||
|
/// A plugin to handle the map generation.
|
||||||
|
pub struct MapGenerationPlugin;
|
||||||
|
|
||||||
|
/// The zoom of the map during the generation.
|
||||||
|
const MAP_GENERATION_SCALE: f32 = 5.;
|
||||||
|
|
||||||
|
impl Plugin for MapGenerationPlugin {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
app.add_event::<StartMapGeneration>()
|
||||||
|
.add_event::<EndMapGeneration>()
|
||||||
|
.add_systems(
|
||||||
|
Update,
|
||||||
|
(delete_map, generate_map.after(delete_map))
|
||||||
|
.run_if(in_state(crate::CurrentScene::Game)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An event to trigger the generation of the map.
|
||||||
|
#[derive(Event, Serialize, Deserialize, Clone, Copy)]
|
||||||
|
pub struct StartMapGeneration {
|
||||||
|
/// The seed used to generate the map.
|
||||||
|
pub seed: u32,
|
||||||
|
|
||||||
|
/// The radius of the map.
|
||||||
|
pub radius: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An event send when the map is generated.
|
||||||
|
#[derive(Event)]
|
||||||
|
pub struct EndMapGeneration;
|
||||||
|
|
||||||
|
/// Generate each tiles of the map if the [StartMapGeneration] is received.
|
||||||
|
///
|
||||||
|
/// The map is generated using a [Perlin] noise and a [HexSpiral].
|
||||||
|
///
|
||||||
|
/// It's generated one tile at a time, until the spiral is finished.
|
||||||
|
fn generate_map(
|
||||||
|
mut start_generation_events: EventReader<StartMapGeneration>,
|
||||||
|
mut end_generation_writer: EventWriter<EndMapGeneration>,
|
||||||
|
mut commands: Commands,
|
||||||
|
mut local_noise: Local<Option<Perlin>>,
|
||||||
|
mut local_spiral: Local<Option<HexSpiral<i32>>>,
|
||||||
|
) {
|
||||||
|
// Handle map generation events and create the spiral and the noise.
|
||||||
|
for event in start_generation_events.read() {
|
||||||
|
*local_noise = Some(Perlin::new(event.seed));
|
||||||
|
*local_spiral = Some(TilePosition::new(0, 0).spiral(event.radius as usize));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the map is being generated.
|
||||||
|
let (Some(noise), Some(spiral)) = (local_noise.as_ref(), local_spiral.as_mut()) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Spawn a tile until the spiral is finished
|
||||||
|
// If the map is generated, we send [EndMapGeneration] and set the local
|
||||||
|
// variables to None.
|
||||||
|
if let Some(position) = spiral.next() {
|
||||||
|
commands.spawn((get_tile_type(position, noise), position as TilePosition));
|
||||||
|
} else {
|
||||||
|
end_generation_writer.send(EndMapGeneration);
|
||||||
|
*local_noise = None;
|
||||||
|
*local_spiral = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the type of the [HexPosition] with the given noise.
|
||||||
|
fn get_tile_type(position: HexPosition<i32>, noise: &Perlin) -> Tile {
|
||||||
|
let pixel_position = position.to_pixel_coordinates() / MAP_GENERATION_SCALE;
|
||||||
|
let value = noise.get([pixel_position.x as f64, pixel_position.y as f64]);
|
||||||
|
match value {
|
||||||
|
v if v <= -0.4 => Tile::Hill,
|
||||||
|
v if v >= 0.4 => Tile::Forest,
|
||||||
|
_ => Tile::Grass,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Despawns the tiles if the event [StartMapGeneration] is received.
|
||||||
|
fn delete_map(
|
||||||
|
mut commands: Commands,
|
||||||
|
query: Query<Entity, With<Tile>>,
|
||||||
|
mut start_generation_events: EventReader<StartMapGeneration>,
|
||||||
|
) {
|
||||||
|
for _ in start_generation_events.read() {
|
||||||
|
for entity in query.iter() {
|
||||||
|
commands.entity(entity).despawn_recursive();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
383
crates/border-wars/src/map/hex.rs
Normal file
|
@ -0,0 +1,383 @@
|
||||||
|
//! All functions related to calculations in a hexagonal grid.
|
||||||
|
|
||||||
|
use std::ops::{
|
||||||
|
Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Rem, RemAssign, Sub, SubAssign,
|
||||||
|
};
|
||||||
|
|
||||||
|
use bevy::prelude::*;
|
||||||
|
use paste::paste;
|
||||||
|
|
||||||
|
/// Represents a number that can be used in calculations for hexagonal grids.
|
||||||
|
pub trait Number:
|
||||||
|
Copy
|
||||||
|
+ PartialEq
|
||||||
|
+ PartialOrd
|
||||||
|
+ Add<Output = Self>
|
||||||
|
+ Sub<Output = Self>
|
||||||
|
+ Mul<Output = Self>
|
||||||
|
+ Div<Output = Self>
|
||||||
|
+ Rem<Output = Self>
|
||||||
|
+ Neg<Output = Self>
|
||||||
|
+ AddAssign
|
||||||
|
+ SubAssign
|
||||||
|
+ MulAssign
|
||||||
|
+ DivAssign
|
||||||
|
+ RemAssign
|
||||||
|
+ std::fmt::Debug
|
||||||
|
{
|
||||||
|
/// The number -2.
|
||||||
|
const MINUS_TWO: Self;
|
||||||
|
|
||||||
|
/// The number -1.
|
||||||
|
const MINUS_ONE: Self;
|
||||||
|
|
||||||
|
/// The number 0.
|
||||||
|
const ZERO: Self;
|
||||||
|
|
||||||
|
/// The number 1.
|
||||||
|
const ONE: Self;
|
||||||
|
|
||||||
|
/// The number 2.
|
||||||
|
const TWO: Self;
|
||||||
|
|
||||||
|
/// Returns the maximum of `self` and `other`.
|
||||||
|
fn max(self, other: Self) -> Self {
|
||||||
|
if self > other { self } else { other }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the minimum of `self` and `other`.
|
||||||
|
fn min(self, other: Self) -> Self {
|
||||||
|
if self < other { self } else { other }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the absolute value of `self`.
|
||||||
|
fn abs(self) -> Self {
|
||||||
|
if self < Self::ZERO { -self } else { self }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converts an `usize` to `Self`.
|
||||||
|
fn from_usize(value: usize) -> Self;
|
||||||
|
|
||||||
|
/// Converts `self` to an `f32`.
|
||||||
|
fn to_f32(self) -> f32;
|
||||||
|
|
||||||
|
/// Converts an `f32` to `Self`.
|
||||||
|
fn from_f32(value: f32) -> Self;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Implements the `Number` trait for the given types.
|
||||||
|
macro_rules! number_impl {
|
||||||
|
($($t:ty,)*) => {paste!{$(
|
||||||
|
impl Number for $t {
|
||||||
|
const MINUS_ONE: Self = - [< 1 $t >];
|
||||||
|
const MINUS_TWO: Self = - [< 2 $t >];
|
||||||
|
const ZERO: Self = [< 0 $t >];
|
||||||
|
const ONE: Self = [< 1 $t >];
|
||||||
|
const TWO: Self = [< 2 $t >];
|
||||||
|
|
||||||
|
|
||||||
|
fn from_usize(value: usize) -> Self {
|
||||||
|
value as $t
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_f32(self) -> f32 {
|
||||||
|
self as f32
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_f32(value: f32) -> Self {
|
||||||
|
value as $t
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)*}};
|
||||||
|
}
|
||||||
|
|
||||||
|
number_impl! {
|
||||||
|
i8, i16, i32, i64, i128, isize,
|
||||||
|
f32, f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents a position in a hexagonal grid.
|
||||||
|
/// We use the axial coordinate system explained in this
|
||||||
|
/// [documentation](https://www.redblobgames.com/grids/hexagons/#coordinates).
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Component)]
|
||||||
|
pub struct HexPosition<T: Number>(pub T, pub T);
|
||||||
|
|
||||||
|
/// All possible directions in a hexagonal grid.
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
|
||||||
|
pub enum HexDirection {
|
||||||
|
/// The direction right.
|
||||||
|
Right,
|
||||||
|
|
||||||
|
/// The direction up-right.
|
||||||
|
UpRight,
|
||||||
|
|
||||||
|
/// The direction up-left.
|
||||||
|
UpLeft,
|
||||||
|
|
||||||
|
/// The direction left.
|
||||||
|
Left,
|
||||||
|
|
||||||
|
/// The direction down-left.
|
||||||
|
DownLeft,
|
||||||
|
|
||||||
|
/// The direction down-right.
|
||||||
|
DownRight,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HexDirection {
|
||||||
|
/// Returns the vector ([HexPosition]) of the direction.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```no_run
|
||||||
|
/// use border_wars::map::hex::{HexDirection, HexPosition};
|
||||||
|
///
|
||||||
|
/// let direction = HexDirection::Right;
|
||||||
|
/// assert_eq!(direction.to_vector(), HexPosition(1, 0));
|
||||||
|
/// ```
|
||||||
|
pub const fn to_vector<T: Number>(self) -> HexPosition<T> {
|
||||||
|
match self {
|
||||||
|
Self::Right => HexPosition(T::ONE, T::ZERO),
|
||||||
|
Self::UpRight => HexPosition(T::ONE, T::MINUS_ONE),
|
||||||
|
Self::UpLeft => HexPosition(T::ZERO, T::MINUS_ONE),
|
||||||
|
Self::Left => HexPosition(T::MINUS_ONE, T::ZERO),
|
||||||
|
Self::DownLeft => HexPosition(T::MINUS_ONE, T::ONE),
|
||||||
|
Self::DownRight => HexPosition(T::ZERO, T::ONE),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A hexagonal ring iterator.
|
||||||
|
pub struct HexRing<T: Number> {
|
||||||
|
/// The current position in the ring.
|
||||||
|
current: HexPosition<T>,
|
||||||
|
|
||||||
|
/// The direction of the current position to the next in the ring.
|
||||||
|
direction: HexDirection,
|
||||||
|
|
||||||
|
/// The radius of the ring.
|
||||||
|
radius: usize,
|
||||||
|
|
||||||
|
/// The index of the current position in the ring.
|
||||||
|
index: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Number> Iterator for HexRing<T> {
|
||||||
|
type Item = HexPosition<T>;
|
||||||
|
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
if self.index >= self.radius {
|
||||||
|
self.direction = match self.direction {
|
||||||
|
HexDirection::Right => HexDirection::UpRight,
|
||||||
|
HexDirection::UpRight => HexDirection::UpLeft,
|
||||||
|
HexDirection::UpLeft => HexDirection::Left,
|
||||||
|
HexDirection::Left => HexDirection::DownLeft,
|
||||||
|
HexDirection::DownLeft => HexDirection::DownRight,
|
||||||
|
HexDirection::DownRight => return None,
|
||||||
|
};
|
||||||
|
self.index = 0;
|
||||||
|
}
|
||||||
|
let result = self.current;
|
||||||
|
self.current += self.direction.to_vector();
|
||||||
|
self.index += 1;
|
||||||
|
Some(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||||
|
let remaining = match self.direction {
|
||||||
|
HexDirection::Right => self.radius * 6,
|
||||||
|
HexDirection::UpRight => self.radius * 5,
|
||||||
|
HexDirection::UpLeft => self.radius * 4,
|
||||||
|
HexDirection::Left => self.radius * 3,
|
||||||
|
HexDirection::DownLeft => self.radius * 2,
|
||||||
|
HexDirection::DownRight => self.radius,
|
||||||
|
} - self.index;
|
||||||
|
(remaining, Some(remaining))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A hexagonal spiral iterator.
|
||||||
|
pub struct HexSpiral<T: Number> {
|
||||||
|
/// The origin of the spiral.
|
||||||
|
origin: HexPosition<T>,
|
||||||
|
|
||||||
|
/// The current ring of the spiral.
|
||||||
|
current: HexRing<T>,
|
||||||
|
|
||||||
|
/// The radius of the spiral.
|
||||||
|
radius: usize,
|
||||||
|
|
||||||
|
/// The index of the current ring in the spiral.
|
||||||
|
index: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Number> Iterator for HexSpiral<T> {
|
||||||
|
type Item = HexPosition<T>;
|
||||||
|
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
// The origin of the spiral.
|
||||||
|
if self.index == 0 {
|
||||||
|
self.index += 1;
|
||||||
|
return Some(self.origin);
|
||||||
|
}
|
||||||
|
if self.index > self.radius {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let mut result = self.current.next();
|
||||||
|
if result.is_none() && self.index < self.radius {
|
||||||
|
self.index += 1;
|
||||||
|
self.current = self.origin.ring(self.index);
|
||||||
|
result = self.current.next();
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Number> HexPosition<T> {
|
||||||
|
/// Creates a new [HexPosition].
|
||||||
|
pub const fn new(x: T, y: T) -> Self {
|
||||||
|
Self(x, y)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converts the current [HexPosition] into a pixel coordinate.
|
||||||
|
///
|
||||||
|
/// If you want to learn more about pixel coordinates conversion,
|
||||||
|
/// you can check the
|
||||||
|
/// [documentation](https://www.redblobgames.com/grids/hexagons/#hex-to-pixel).
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```no_run
|
||||||
|
/// use bevy::math::Vec2;
|
||||||
|
/// use border_wars::map::hex::HexPosition;
|
||||||
|
///
|
||||||
|
/// let position = HexPosition(1, 0);
|
||||||
|
/// assert_eq!(position.to_pixel_coordinates(), (3f32.sqrt(), 0.0).into());
|
||||||
|
/// ```
|
||||||
|
pub fn to_pixel_coordinates(&self) -> Vec2 {
|
||||||
|
Vec2::new(
|
||||||
|
3f32.sqrt()
|
||||||
|
.mul_add(T::to_f32(self.0), 3f32.sqrt() / 2.0 * T::to_f32(self.1)),
|
||||||
|
3.0 / 2.0 * T::to_f32(self.1),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the distance between two [HexPosition]s.
|
||||||
|
///
|
||||||
|
/// # How it works
|
||||||
|
///
|
||||||
|
/// In the hexagonal grid, using the
|
||||||
|
/// [cube coordinate system](https://www.redblobgames.com/grids/hexagons/#coordinates),
|
||||||
|
/// it's akin to a cube in 3D space.
|
||||||
|
/// The Manhattan distance between two positions is equal to half of
|
||||||
|
/// the sum of abs(dx) + abs(dy) + abs(dz).
|
||||||
|
/// However, in hexagonal grids, z is defined as -q - r.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```no_run
|
||||||
|
/// use border_wars::map::hex::HexPosition;
|
||||||
|
///
|
||||||
|
/// let a = HexPosition(0, 0);
|
||||||
|
/// let b = HexPosition(1, 1);
|
||||||
|
///
|
||||||
|
/// assert_eq!(a.distance(b), 2);
|
||||||
|
/// ```
|
||||||
|
pub fn distance(self, other: Self) -> T {
|
||||||
|
let Self(x, y) = self - other;
|
||||||
|
x.abs() + y.abs() + (x + y).abs() / T::TWO
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the hexagonal ring of the given radius.
|
||||||
|
/// If you want to learn more about hexagonal grids, check the
|
||||||
|
/// [documentation](https://www.redblobgames.com/grids/hexagons/#rings)
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```no_run
|
||||||
|
/// use border_wars::map::hex::HexPosition;
|
||||||
|
///
|
||||||
|
/// let position = HexPosition(0, 0);
|
||||||
|
/// let radius = 1;
|
||||||
|
///
|
||||||
|
/// for ring_position in position.ring(radius) {
|
||||||
|
/// println!("{:?}", ring_position);
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
pub fn ring(self, radius: usize) -> HexRing<T> {
|
||||||
|
HexRing {
|
||||||
|
current: self + HexDirection::DownLeft.to_vector() * T::from_usize(radius),
|
||||||
|
direction: HexDirection::Right,
|
||||||
|
radius,
|
||||||
|
index: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the hexagonal spiral of the given radius.
|
||||||
|
/// If you want to learn more about hexagonal grids, check the
|
||||||
|
/// [documentation](https://www.redblobgames.com/grids/hexagons/#rings-spiral)
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```no_run
|
||||||
|
/// use border_wars::map::hex::HexPosition;
|
||||||
|
///
|
||||||
|
/// let position = HexPosition(0, 0);
|
||||||
|
/// let radius = 1;
|
||||||
|
///
|
||||||
|
/// for spiral_position in position.spiral(radius) {
|
||||||
|
/// println!("{:?}", spiral_position);
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
pub fn spiral(self, radius: usize) -> HexSpiral<T> {
|
||||||
|
HexSpiral {
|
||||||
|
origin: self,
|
||||||
|
current: self.ring(1),
|
||||||
|
radius,
|
||||||
|
index: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Implementation of the arithmetic operators for hexagonal positions.
|
||||||
|
macro_rules! impl_ops {
|
||||||
|
($(($t:ty, $n:ident),)*) => {paste!{$(
|
||||||
|
impl<T: Number> $t for HexPosition<T> {
|
||||||
|
type Output = Self;
|
||||||
|
|
||||||
|
fn $n(self, rhs: Self) -> Self {
|
||||||
|
Self(self.0.$n(rhs.0), self.1.$n(rhs.1))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Number> $t<T> for HexPosition<T> {
|
||||||
|
type Output = Self;
|
||||||
|
|
||||||
|
fn $n(self, rhs: T) -> Self {
|
||||||
|
Self(self.0.$n(rhs), self.1.$n(rhs))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Number> [< $t Assign >] for HexPosition<T> {
|
||||||
|
fn [< $n _assign >](&mut self, rhs: Self) {
|
||||||
|
self.0.[< $n _assign >](rhs.0) ;
|
||||||
|
self.1.[< $n _assign >](rhs.1) ;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Number> [< $t Assign >]<T> for HexPosition<T> {
|
||||||
|
fn [< $n _assign >](&mut self, rhs: T) {
|
||||||
|
self.0.[< $n _assign >](rhs);
|
||||||
|
self.1.[< $n _assign >](rhs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)*}};
|
||||||
|
}
|
||||||
|
|
||||||
|
impl_ops! {
|
||||||
|
(Add, add),
|
||||||
|
(Sub, sub),
|
||||||
|
(Mul, mul),
|
||||||
|
(Div, div),
|
||||||
|
(Rem, rem),
|
||||||
|
}
|
70
crates/border-wars/src/map/mod.rs
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
//! Contains all the logic related to the map.
|
||||||
|
|
||||||
|
pub mod generation;
|
||||||
|
pub mod hex;
|
||||||
|
pub mod ownership;
|
||||||
|
pub mod renderer;
|
||||||
|
pub mod selected_tile;
|
||||||
|
|
||||||
|
use bevy::prelude::*;
|
||||||
|
|
||||||
|
use self::hex::*;
|
||||||
|
|
||||||
|
/// The position of a tile in a hexagonal map.
|
||||||
|
pub type TilePosition = HexPosition<i32>;
|
||||||
|
|
||||||
|
/// The tile of the map.
|
||||||
|
#[derive(Component, Debug)]
|
||||||
|
pub enum Tile {
|
||||||
|
/// The breeding tile.
|
||||||
|
Breeding,
|
||||||
|
|
||||||
|
/// The Casern tile.
|
||||||
|
Casern,
|
||||||
|
|
||||||
|
/// The castle tile.
|
||||||
|
Castle,
|
||||||
|
|
||||||
|
/// The hill tile.
|
||||||
|
Hill,
|
||||||
|
|
||||||
|
/// The grass tile.
|
||||||
|
Grass,
|
||||||
|
|
||||||
|
/// The forest tile.
|
||||||
|
Forest,
|
||||||
|
|
||||||
|
/// The mine tile.
|
||||||
|
Mine,
|
||||||
|
|
||||||
|
/// The outpost tile
|
||||||
|
Outpost,
|
||||||
|
|
||||||
|
/// The sawmill tile
|
||||||
|
Sawmill,
|
||||||
|
|
||||||
|
/// The tower tile
|
||||||
|
Tower,
|
||||||
|
|
||||||
|
/// The wall tile
|
||||||
|
Wall,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Tile {
|
||||||
|
/// Returns the text representation of the tile.
|
||||||
|
pub fn to_text(&self) -> String {
|
||||||
|
match self {
|
||||||
|
Self::Breeding => "breeding".to_string(),
|
||||||
|
Self::Casern => "casern".to_string(),
|
||||||
|
Self::Castle => "castle".to_string(),
|
||||||
|
Self::Forest => "forest".to_string(),
|
||||||
|
Self::Grass => "grass".to_string(),
|
||||||
|
Self::Hill => "hill".to_string(),
|
||||||
|
Self::Mine => "mine".to_string(),
|
||||||
|
Self::Outpost => "outpost".to_string(),
|
||||||
|
Self::Sawmill => "sawmill".to_string(),
|
||||||
|
Self::Tower => "tower".to_string(),
|
||||||
|
Self::Wall => "wall".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
56
crates/border-wars/src/map/ownership.rs
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
//! All code related to the ownership of the tiles.
|
||||||
|
|
||||||
|
use bevy::prelude::*;
|
||||||
|
|
||||||
|
use crate::Player;
|
||||||
|
|
||||||
|
/// The owner of a tile.
|
||||||
|
#[derive(Component, Clone)]
|
||||||
|
pub struct Owner(pub Player);
|
||||||
|
|
||||||
|
/// The plugin to render the ownership of the tiles.
|
||||||
|
pub struct OwnershipPlugin;
|
||||||
|
|
||||||
|
impl Plugin for OwnershipPlugin {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
app.add_systems(Update, render_ownership);
|
||||||
|
app.add_systems(Startup, setup_ownership_resources);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The contrast of the ownership colors.
|
||||||
|
///
|
||||||
|
/// The value is a number between 0 and 1.
|
||||||
|
#[derive(Resource)]
|
||||||
|
pub struct OwnershipColorContrast(pub f32);
|
||||||
|
|
||||||
|
/// Init resources related to the ownership of the tiles.
|
||||||
|
fn setup_ownership_resources(mut commands: Commands) {
|
||||||
|
commands.insert_resource(OwnershipColorContrast(0.4));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The type condition for update ownership.
|
||||||
|
type OwnershipUpdate = Or<(Changed<Owner>, Changed<Sprite>)>;
|
||||||
|
|
||||||
|
/// Render the ownership of the tiles by applying colors.
|
||||||
|
fn render_ownership(
|
||||||
|
mut query: Query<(&mut Sprite, &Owner), OwnershipUpdate>,
|
||||||
|
contrast: Res<OwnershipColorContrast>,
|
||||||
|
) {
|
||||||
|
for (mut sprite, owner) in query.iter_mut() {
|
||||||
|
let (r, g, b) = owner.0.color;
|
||||||
|
let target = mix_colors(Color::rgb_u8(r, g, b), sprite.color, 1. - contrast.0);
|
||||||
|
|
||||||
|
sprite.color = target;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mixes two colors.
|
||||||
|
fn mix_colors(color1: Color, color2: Color, alpha: f32) -> Color {
|
||||||
|
let [r1, g1, b1, _] = color1.as_rgba_u8();
|
||||||
|
let [r2, g2, b2, _] = color2.as_rgba_u8();
|
||||||
|
let mixed_r = (1.0 - alpha).mul_add(r1 as f32, alpha * r2 as f32).round() as u8;
|
||||||
|
let mixed_g = (1.0 - alpha).mul_add(g1 as f32, alpha * g2 as f32).round() as u8;
|
||||||
|
let mixed_b = (1.0 - alpha).mul_add(b1 as f32, alpha * b2 as f32).round() as u8;
|
||||||
|
Color::rgb_u8(mixed_r, mixed_g, mixed_b)
|
||||||
|
}
|
106
crates/border-wars/src/map/renderer.rs
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
//! All functions related to the rendering of the map.
|
||||||
|
|
||||||
|
use bevy::prelude::*;
|
||||||
|
use bevy::sprite::Anchor;
|
||||||
|
|
||||||
|
use crate::map::{Tile, TilePosition};
|
||||||
|
|
||||||
|
/// A plugin to render the map.
|
||||||
|
pub struct RendererPlugin;
|
||||||
|
|
||||||
|
impl Plugin for RendererPlugin {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
app.add_systems(Startup, init_resources_for_rendering)
|
||||||
|
.add_systems(
|
||||||
|
Update,
|
||||||
|
render_map.run_if(in_state(crate::CurrentScene::Game)),
|
||||||
|
)
|
||||||
|
.insert_resource(ClearColor(Color::rgb_u8(129, 212, 250)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The gap between the center of the tiles in the map.
|
||||||
|
#[derive(Resource)]
|
||||||
|
pub struct TilesGap(pub Vec2);
|
||||||
|
|
||||||
|
/// The size of the tiles in the map.
|
||||||
|
#[derive(Resource, Clone, Copy)]
|
||||||
|
struct TilesSize(Vec2);
|
||||||
|
|
||||||
|
impl Tile {
|
||||||
|
/// Returns the handle of the image of the tile.
|
||||||
|
fn get_texture(&self, asset_server: &AssetServer) -> Handle<Image> {
|
||||||
|
asset_server.load(format!("tiles/{}.png", self.to_text()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the size of the image of the tile.
|
||||||
|
///
|
||||||
|
/// TODO: we are currently using temporary images that will modify
|
||||||
|
/// this function in the future.
|
||||||
|
pub const fn get_image_size(&self) -> Vec2 {
|
||||||
|
match self {
|
||||||
|
Self::Breeding => Vec2::new(184., 158.),
|
||||||
|
Self::Casern => Vec2::new(184., 167.),
|
||||||
|
Self::Castle => Vec2::new(192., 196.),
|
||||||
|
Self::Forest => Vec2::new(184., 165.),
|
||||||
|
Self::Grass => Vec2::new(184., 138.),
|
||||||
|
Self::Hill => Vec2::new(184., 181.),
|
||||||
|
Self::Mine => Vec2::new(184., 166.),
|
||||||
|
Self::Outpost => Vec2::new(184., 208.),
|
||||||
|
Self::Sawmill => Vec2::new(184., 138.),
|
||||||
|
Self::Tower => Vec2::new(184., 218.),
|
||||||
|
Self::Wall => Vec2::new(184., 186.),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Init resources related to the rendering of the map.
|
||||||
|
fn init_resources_for_rendering(mut commands: Commands) {
|
||||||
|
commands.insert_resource(TilesGap(Vec2 { x: 70., y: 35. }));
|
||||||
|
commands.insert_resource(TilesSize(Vec2 { x: 125., y: 100. }))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Renders the map.
|
||||||
|
fn render_map(
|
||||||
|
query: Query<(Entity, &TilePosition, &Tile), Changed<Tile>>,
|
||||||
|
mut commands: Commands,
|
||||||
|
asset_server: Res<AssetServer>,
|
||||||
|
tiles_gap: Res<TilesGap>,
|
||||||
|
tiles_size: Res<TilesSize>,
|
||||||
|
) {
|
||||||
|
for (entity, position, tile) in query.iter() {
|
||||||
|
let texture = tile.get_texture(&asset_server);
|
||||||
|
|
||||||
|
let translation_2d = tiles_gap.0 * position.to_pixel_coordinates();
|
||||||
|
let translation = Vec3::new(
|
||||||
|
translation_2d.x,
|
||||||
|
translation_2d.y,
|
||||||
|
z_position_from_y(translation_2d.y),
|
||||||
|
);
|
||||||
|
|
||||||
|
let scale_2d = tiles_size.0 / tile.get_image_size();
|
||||||
|
|
||||||
|
// the y scale is the same as the x scale to keep the aspect ratio.
|
||||||
|
let scale = Vec3::new(scale_2d.x, scale_2d.x, 1.0);
|
||||||
|
|
||||||
|
commands.entity(entity).insert(SpriteBundle {
|
||||||
|
sprite: Sprite {
|
||||||
|
anchor: Anchor::BottomCenter,
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
texture,
|
||||||
|
transform: Transform {
|
||||||
|
translation,
|
||||||
|
scale,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
..default()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A simple sigmoid function to convert y position to z position.
|
||||||
|
/// The return value is between 0 and 1.
|
||||||
|
fn z_position_from_y(y: f32) -> f32 {
|
||||||
|
-1.0 / (1.0 + (-y * 110_f64.powi(-3) as f32).exp())
|
||||||
|
}
|
131
crates/border-wars/src/map/selected_tile.rs
Normal file
|
@ -0,0 +1,131 @@
|
||||||
|
//! All programs related to the selection of a tile.
|
||||||
|
|
||||||
|
use bevy::prelude::*;
|
||||||
|
|
||||||
|
use super::renderer::TilesGap;
|
||||||
|
use super::Tile;
|
||||||
|
|
||||||
|
/// An event that is triggered when a mouse button is clicked.
|
||||||
|
///
|
||||||
|
/// The event contains the position of the cursor in the world.
|
||||||
|
#[derive(Event)]
|
||||||
|
struct ClickOnTheWorld(Vec2);
|
||||||
|
|
||||||
|
/// A zone that can't be clicked.
|
||||||
|
/// For exemple the UI of the game.
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct ZoneNotClickable;
|
||||||
|
|
||||||
|
/// The currently selected tile.
|
||||||
|
#[derive(Resource, Default, Debug)]
|
||||||
|
pub enum SelectedTile {
|
||||||
|
/// The entity of the selected tile.
|
||||||
|
Tile(Entity),
|
||||||
|
|
||||||
|
/// Zero tile selected.
|
||||||
|
#[default]
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SelectedTile {
|
||||||
|
/// Returns the entity of the selected tile.
|
||||||
|
/// Returns `None` if no tile is selected.
|
||||||
|
pub const fn get_entity(&self) -> Option<Entity> {
|
||||||
|
match self {
|
||||||
|
Self::Tile(entity) => Some(*entity),
|
||||||
|
Self::None => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A plugin that handles the selection of tiles.
|
||||||
|
pub struct SelectTilePlugin;
|
||||||
|
|
||||||
|
impl Plugin for SelectTilePlugin {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
app.add_systems(PreUpdate, mouse_handler)
|
||||||
|
.add_systems(PreUpdate, select_closest_tile)
|
||||||
|
.add_event::<ClickOnTheWorld>()
|
||||||
|
.init_resource::<SelectedTile>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handles the mouse click and gets the position of the cursor in the world.
|
||||||
|
/// Finally, it sends an event with the position of the cursor.
|
||||||
|
fn mouse_handler(
|
||||||
|
mouse_button_input: Res<Input<MouseButton>>,
|
||||||
|
windows: Query<&Window>,
|
||||||
|
cameras: Query<(&Camera, &GlobalTransform)>,
|
||||||
|
mut events_writer: EventWriter<ClickOnTheWorld>,
|
||||||
|
not_clickable_zones: Query<(&Node, &GlobalTransform), With<ZoneNotClickable>>,
|
||||||
|
ui_scale: Res<UiScale>,
|
||||||
|
) {
|
||||||
|
if !mouse_button_input.just_pressed(MouseButton::Left) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let window = windows.get_single().expect("Main window not found");
|
||||||
|
|
||||||
|
let cursor_position_on_screen = window.cursor_position();
|
||||||
|
|
||||||
|
let Some(cursor_position_on_screen) = cursor_position_on_screen else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
for (node, global_transform) in not_clickable_zones.iter() {
|
||||||
|
let rect = node.physical_rect(global_transform, window.scale_factor(), ui_scale.0);
|
||||||
|
if rect.contains(cursor_position_on_screen) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let (camera, camera_transform) = cameras.get_single().expect("Camera not found");
|
||||||
|
|
||||||
|
let cursor_position_in_world = camera
|
||||||
|
.viewport_to_world(camera_transform, cursor_position_on_screen)
|
||||||
|
.expect("Failed to convert cursor position")
|
||||||
|
.origin
|
||||||
|
.truncate();
|
||||||
|
|
||||||
|
events_writer.send(ClickOnTheWorld(cursor_position_in_world));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the closest tile to the cursor and select it.
|
||||||
|
fn select_closest_tile(
|
||||||
|
tiles: Query<(Entity, &Transform, &Tile)>,
|
||||||
|
mut click_event_reader: EventReader<ClickOnTheWorld>,
|
||||||
|
tile_gap: Res<TilesGap>,
|
||||||
|
mut current_entity: ResMut<SelectedTile>,
|
||||||
|
) {
|
||||||
|
for click_event in click_event_reader.read() {
|
||||||
|
// The closest tile and its position.
|
||||||
|
let mut closest_entity: Option<Entity> = None;
|
||||||
|
let mut closest_position: Option<f32> = None;
|
||||||
|
|
||||||
|
// To keep the aspect ratio.
|
||||||
|
let click_position = click_event.0 / tile_gap.0;
|
||||||
|
|
||||||
|
for (tile_entity, tile_transform, tile_type) in tiles.iter() {
|
||||||
|
let tile_size = tile_type.get_image_size();
|
||||||
|
let tile_scale = tile_transform.scale.truncate();
|
||||||
|
|
||||||
|
let mut tile_position = tile_transform.translation.truncate() / tile_gap.0;
|
||||||
|
// The origine of the tile is the bottom center.
|
||||||
|
tile_position.y += (tile_size.y / 2.0) * tile_scale.y / tile_gap.0.y;
|
||||||
|
|
||||||
|
let distance_to_cursor = tile_position.distance(click_position);
|
||||||
|
|
||||||
|
if closest_position.is_none() || closest_position > Some(distance_to_cursor) {
|
||||||
|
closest_entity = Some(tile_entity);
|
||||||
|
closest_position = Some(distance_to_cursor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(tile_entity) = closest_entity {
|
||||||
|
if current_entity.get_entity() == Some(tile_entity) {
|
||||||
|
*current_entity = SelectedTile::None;
|
||||||
|
} else {
|
||||||
|
*current_entity = SelectedTile::Tile(tile_entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
118
crates/border-wars/src/networking/check_connection.rs
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
//! All the code related to the check connection (check every X seconds if any
|
||||||
|
//! player is still connected).
|
||||||
|
|
||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
|
use bevnet::{Connection, NetworkAppExt, Receive, SendTo};
|
||||||
|
use bevy::prelude::*;
|
||||||
|
use bevy::utils::HashMap;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::Player;
|
||||||
|
|
||||||
|
/// A plugin that check if a player is still connected.
|
||||||
|
pub struct CheckConnectionPlugin;
|
||||||
|
|
||||||
|
/// An event that is trigger when a player is disconnected.
|
||||||
|
#[derive(Event)]
|
||||||
|
pub struct PlayerDisconnected(pub Player);
|
||||||
|
|
||||||
|
/// An event that is send between all players to check if a player is still
|
||||||
|
/// connected.
|
||||||
|
#[derive(Event, Serialize, Deserialize)]
|
||||||
|
struct IAmConnected(Player);
|
||||||
|
|
||||||
|
impl Plugin for CheckConnectionPlugin {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
app.add_systems(
|
||||||
|
Update,
|
||||||
|
(
|
||||||
|
check_connection,
|
||||||
|
send_check_connection,
|
||||||
|
handle_disconnect_player,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.add_event::<PlayerDisconnected>()
|
||||||
|
.add_network_event::<IAmConnected>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The interval to check if a player is still connected.
|
||||||
|
/// We put this into a const because we don't want to change it.
|
||||||
|
const CHECK_CONNECTION_INTERVAL: std::time::Duration = std::time::Duration::from_secs(5);
|
||||||
|
|
||||||
|
/// A fonction that check if a player is still connected.
|
||||||
|
fn check_connection(
|
||||||
|
all_players_query: Query<&Player>,
|
||||||
|
mut disconnect_event: EventWriter<PlayerDisconnected>,
|
||||||
|
mut checked_players: Local<HashMap<Player, Instant>>,
|
||||||
|
mut connect_event: EventReader<Receive<IAmConnected>>,
|
||||||
|
) {
|
||||||
|
for Receive(_, IAmConnected(player)) in connect_event.read() {
|
||||||
|
checked_players.insert(player.clone(), Instant::now());
|
||||||
|
}
|
||||||
|
for player in all_players_query.iter() {
|
||||||
|
if !(*checked_players).contains_key(player) {
|
||||||
|
checked_players.insert(player.clone(), Instant::now());
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(last_seen) = (*checked_players).get_mut(player) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if last_seen.elapsed() > CHECK_CONNECTION_INTERVAL {
|
||||||
|
disconnect_event.send(PlayerDisconnected(player.clone()));
|
||||||
|
checked_players.remove(player);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A simple time/instant that implement Default.
|
||||||
|
struct Time(std::time::Instant);
|
||||||
|
|
||||||
|
impl Default for Time {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self(std::time::Instant::now())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A fonction that send a check connection event to all players.
|
||||||
|
fn send_check_connection(
|
||||||
|
mut check_connection_event: EventWriter<SendTo<IAmConnected>>,
|
||||||
|
all_players_query: Query<&Player>,
|
||||||
|
connection: Res<Connection>,
|
||||||
|
mut timer: Local<Time>,
|
||||||
|
) {
|
||||||
|
if timer.0.elapsed() < CHECK_CONNECTION_INTERVAL / 2 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let Some(self_player) = all_players_query
|
||||||
|
.iter()
|
||||||
|
.find(|player| connection.identifier() == Some(player.uuid))
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
for player in all_players_query.iter() {
|
||||||
|
check_connection_event.send(SendTo(player.uuid, IAmConnected(self_player.clone())));
|
||||||
|
}
|
||||||
|
|
||||||
|
timer.0 = std::time::Instant::now();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A fonction that handle player disconnection.
|
||||||
|
fn handle_disconnect_player(
|
||||||
|
mut disconnect_players: EventReader<PlayerDisconnected>,
|
||||||
|
all_players_query: Query<(&Player, Entity)>,
|
||||||
|
mut commands: Commands,
|
||||||
|
) {
|
||||||
|
for PlayerDisconnected(disconnect_player) in disconnect_players.read() {
|
||||||
|
let Some((_, entity)) = all_players_query
|
||||||
|
.iter()
|
||||||
|
.find(|(player, _entity)| *player == disconnect_player)
|
||||||
|
else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
commands.entity(entity).despawn();
|
||||||
|
}
|
||||||
|
}
|
95
crates/border-wars/src/networking/connection.rs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
55
crates/border-wars/src/networking/mod.rs
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
//! All the code related to the networking.
|
||||||
|
|
||||||
|
use bevnet::{NetworkAppExt, NetworkPlugin, Receive};
|
||||||
|
use bevy::prelude::*;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use self::check_connection::CheckConnectionPlugin;
|
||||||
|
use self::connection::ConnectionPlugin;
|
||||||
|
use crate::map::generation::StartMapGeneration;
|
||||||
|
use crate::CurrentScene;
|
||||||
|
|
||||||
|
pub mod check_connection;
|
||||||
|
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)
|
||||||
|
.add_systems(Update, handle_start_game)
|
||||||
|
.add_network_event::<StartGame>()
|
||||||
|
.add_plugins(CheckConnectionPlugin);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The rank of the player.
|
||||||
|
#[derive(PartialEq, Eq, Serialize, Deserialize, Clone, Copy, Debug, Hash)]
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The event to start the game, that is send by the admin.
|
||||||
|
#[derive(Event, Serialize, Deserialize)]
|
||||||
|
pub struct StartGame(pub StartMapGeneration);
|
||||||
|
|
||||||
|
/// A fonction that handle the start of the game.
|
||||||
|
fn handle_start_game(
|
||||||
|
mut next_stats: ResMut<NextState<CurrentScene>>,
|
||||||
|
mut start_game_events: EventReader<Receive<StartGame>>,
|
||||||
|
mut start_map_generation_writer: EventWriter<StartMapGeneration>,
|
||||||
|
) {
|
||||||
|
for event in start_game_events.read() {
|
||||||
|
next_stats.set(CurrentScene::Game);
|
||||||
|
start_map_generation_writer.send(event.1.0);
|
||||||
|
}
|
||||||
|
}
|
52
crates/border-wars/src/resources.rs
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
//! All program related to the resources of the game.
|
||||||
|
|
||||||
|
use bevy::prelude::*;
|
||||||
|
|
||||||
|
/// The plugin that manage the resources.
|
||||||
|
pub struct ResourcesPlugin;
|
||||||
|
|
||||||
|
impl Plugin for ResourcesPlugin {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
app.add_event::<ResetResources>()
|
||||||
|
.insert_resource(Resources::initial())
|
||||||
|
.add_systems(Update, handle_reset_resources);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The resources of the game.
|
||||||
|
#[derive(Resource, Default)]
|
||||||
|
pub struct Resources {
|
||||||
|
/// The stone resource.
|
||||||
|
pub stone: u32,
|
||||||
|
|
||||||
|
/// The wood resource.
|
||||||
|
pub wood: u32,
|
||||||
|
|
||||||
|
/// The food resource.
|
||||||
|
pub food: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Resources {
|
||||||
|
/// Returns the initial resources of the game.
|
||||||
|
const fn initial() -> Self {
|
||||||
|
Self {
|
||||||
|
stone: 100,
|
||||||
|
wood: 100,
|
||||||
|
food: 100,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An event send to reset the resources of the game.
|
||||||
|
#[derive(Event)]
|
||||||
|
pub struct ResetResources;
|
||||||
|
|
||||||
|
/// Handles the reset resources event.
|
||||||
|
fn handle_reset_resources(
|
||||||
|
mut reset_resources_event: EventReader<ResetResources>,
|
||||||
|
mut resources: ResMut<Resources>,
|
||||||
|
) {
|
||||||
|
for _ in reset_resources_event.read() {
|
||||||
|
*resources = Resources::initial();
|
||||||
|
}
|
||||||
|
}
|
92
crates/border-wars/src/scenes/lobby.rs
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
//! The lobby of the game.
|
||||||
|
|
||||||
|
use bevnet::{Connection, SendTo};
|
||||||
|
use bevy::prelude::*;
|
||||||
|
use bevy_egui::{egui, EguiContexts};
|
||||||
|
use rand::Rng;
|
||||||
|
|
||||||
|
use crate::map::generation::StartMapGeneration;
|
||||||
|
use crate::networking::connection::RemovePlayer;
|
||||||
|
use crate::networking::{PlayerRank, StartGame};
|
||||||
|
use crate::{CurrentScene, Player};
|
||||||
|
|
||||||
|
/// The plugin for the lobby.
|
||||||
|
pub struct LobbyPlugin;
|
||||||
|
|
||||||
|
impl Plugin for LobbyPlugin {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
app.add_systems(Update, lobby_ui.run_if(in_state(CurrentScene::Lobby)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Display the UI of the lobby.
|
||||||
|
fn lobby_ui(
|
||||||
|
mut ctx: EguiContexts,
|
||||||
|
connection: Res<Connection>,
|
||||||
|
all_players_query: Query<&Player>,
|
||||||
|
mut kick_player: EventWriter<SendTo<RemovePlayer>>,
|
||||||
|
mut map_size: Local<u32>,
|
||||||
|
mut start_game_event: EventWriter<SendTo<StartGame>>,
|
||||||
|
) {
|
||||||
|
// 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");
|
||||||
|
|
||||||
|
ui.separator();
|
||||||
|
|
||||||
|
ui.label("Game created");
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
if self_player.rank != PlayerRank::Admin {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ui.label("Game ID: ");
|
||||||
|
ui.text_edit_singleline(&mut connection.identifier().unwrap_or_default().to_string());
|
||||||
|
});
|
||||||
|
|
||||||
|
ui.separator();
|
||||||
|
|
||||||
|
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 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.add(egui::Slider::new(&mut (*map_size), 1..=3).text("map size"));
|
||||||
|
|
||||||
|
if !ui.button("Run the game").clicked() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let seed = rand::thread_rng().gen::<u32>();
|
||||||
|
let index = *map_size as u16;
|
||||||
|
let nomber_of_players = all_players_query.iter().count() as u32;
|
||||||
|
|
||||||
|
let radius = nomber_of_players as u16 * 2 * (index + 1);
|
||||||
|
|
||||||
|
// Start the game.
|
||||||
|
for player in all_players_query.iter() {
|
||||||
|
start_game_event.send(SendTo(
|
||||||
|
player.uuid,
|
||||||
|
StartGame(StartMapGeneration { seed, radius }),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
79
crates/border-wars/src/scenes/menu.rs
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
//! The main menu of the game.
|
||||||
|
|
||||||
|
use bevnet::{Connection, SendTo, Uuid};
|
||||||
|
use bevy::prelude::*;
|
||||||
|
use bevy_egui::{egui, EguiContexts};
|
||||||
|
|
||||||
|
use crate::networking::connection::RequestJoin;
|
||||||
|
use crate::networking::PlayerRank;
|
||||||
|
use crate::{CurrentScene, Player};
|
||||||
|
|
||||||
|
/// The plugin for the menu.
|
||||||
|
pub struct MenuPlugin;
|
||||||
|
|
||||||
|
impl Plugin for MenuPlugin {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
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::Lobby);
|
||||||
|
request_join.send(SendTo(
|
||||||
|
game_id,
|
||||||
|
RequestJoin(Player {
|
||||||
|
name: name.clone(),
|
||||||
|
rank: PlayerRank::Player,
|
||||||
|
uuid,
|
||||||
|
color: rand::random::<(u8, u8, u8)>(),
|
||||||
|
}),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ui.separator();
|
||||||
|
|
||||||
|
if ui.button("Create new game").clicked() {
|
||||||
|
next_scene.set(CurrentScene::Lobby);
|
||||||
|
commands.spawn(Player {
|
||||||
|
name: name.clone(),
|
||||||
|
rank: PlayerRank::Admin,
|
||||||
|
uuid,
|
||||||
|
color: rand::random::<(u8, u8, u8)>(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
21
crates/border-wars/src/scenes/mod.rs
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
//! The file containing all scenes programs.
|
||||||
|
|
||||||
|
use bevy::prelude::*;
|
||||||
|
use bevy_egui::EguiPlugin;
|
||||||
|
|
||||||
|
use crate::CurrentScene;
|
||||||
|
|
||||||
|
pub mod lobby;
|
||||||
|
pub mod menu;
|
||||||
|
|
||||||
|
/// The plugin for all scenes.
|
||||||
|
pub struct ScenesPlugin;
|
||||||
|
|
||||||
|
impl Plugin for ScenesPlugin {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
app.add_plugins(EguiPlugin)
|
||||||
|
.add_state::<CurrentScene>()
|
||||||
|
.add_plugins(menu::MenuPlugin)
|
||||||
|
.add_plugins(lobby::LobbyPlugin);
|
||||||
|
}
|
||||||
|
}
|
38
crates/border-wars/src/ui/hover.rs
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
//! The file that contains the hover logic.
|
||||||
|
|
||||||
|
use bevy::prelude::*;
|
||||||
|
|
||||||
|
/// The plugin for the hover system.
|
||||||
|
pub struct HoverPlugin;
|
||||||
|
|
||||||
|
impl Plugin for HoverPlugin {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
app.add_systems(Update, hovering);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A component that stores the hover texture and the original texture.
|
||||||
|
#[derive(Component, Clone)]
|
||||||
|
pub struct HoveredTexture {
|
||||||
|
/// The original texture.
|
||||||
|
pub texture: Handle<Image>,
|
||||||
|
|
||||||
|
/// The hovered texture.
|
||||||
|
pub hovered_texture: Handle<Image>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The system that applies the hover logic by changing the texture.
|
||||||
|
fn hovering(
|
||||||
|
mut interaction_query: Query<
|
||||||
|
(&Interaction, &HoveredTexture, &mut UiImage),
|
||||||
|
Changed<Interaction>,
|
||||||
|
>,
|
||||||
|
) {
|
||||||
|
for (interaction, textures, mut image) in interaction_query.iter_mut() {
|
||||||
|
match *interaction {
|
||||||
|
Interaction::Hovered => image.texture = textures.hovered_texture.clone(),
|
||||||
|
Interaction::None => image.texture = textures.texture.clone(),
|
||||||
|
Interaction::Pressed => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
19
crates/border-wars/src/ui/mod.rs
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
//! The file that contains the UI logic.
|
||||||
|
|
||||||
|
pub mod hover;
|
||||||
|
pub mod responsive_scale;
|
||||||
|
|
||||||
|
use bevy::prelude::*;
|
||||||
|
|
||||||
|
use self::hover::HoverPlugin;
|
||||||
|
use self::responsive_scale::ResponsiveScalingPlugin;
|
||||||
|
|
||||||
|
/// The plugin for the UI.
|
||||||
|
pub struct UiPlugin;
|
||||||
|
|
||||||
|
impl Plugin for UiPlugin {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
app.add_plugins(HoverPlugin)
|
||||||
|
.add_plugins(ResponsiveScalingPlugin);
|
||||||
|
}
|
||||||
|
}
|
40
crates/border-wars/src/ui/responsive_scale.rs
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
//! The file that contains the responsive scaling logic.
|
||||||
|
|
||||||
|
use bevy::prelude::*;
|
||||||
|
|
||||||
|
/// The plugin for the responsive scaling.
|
||||||
|
pub struct ResponsiveScalingPlugin;
|
||||||
|
|
||||||
|
impl Plugin for ResponsiveScalingPlugin {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
app.add_systems(Startup, init_window_size);
|
||||||
|
app.add_systems(Update, change_scaling);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The default ui layout size.
|
||||||
|
#[derive(Resource)]
|
||||||
|
pub struct UILayoutSize(pub Vec2);
|
||||||
|
|
||||||
|
/// Initializes [UILayoutSize].
|
||||||
|
pub fn init_window_size(mut command: Commands) {
|
||||||
|
command.insert_resource(UILayoutSize(Vec2::new(1280., 720.)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calculates the ui_scale.0 depending on the [UILayoutSize]
|
||||||
|
/// in order to make the ui layout responsive.
|
||||||
|
pub fn change_scaling(
|
||||||
|
mut ui_scale: ResMut<UiScale>,
|
||||||
|
windows: Query<&Window>,
|
||||||
|
size: Res<UILayoutSize>,
|
||||||
|
) {
|
||||||
|
let window = windows.get_single().expect("Main window not found");
|
||||||
|
if window.resolution.physical_height() == 0 {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let (a, b) = (
|
||||||
|
window.resolution.width() / size.0.x,
|
||||||
|
window.resolution.height() / size.0.y,
|
||||||
|
);
|
||||||
|
ui_scale.0 = if a < b { a } else { b } as f64
|
||||||
|
}
|
20
crates/relay-client/Cargo.toml
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
[package]
|
||||||
|
name = "relay-client"
|
||||||
|
version = "0.2.0"
|
||||||
|
edition = "2021"
|
||||||
|
license = "GPL-3.0-or-later"
|
||||||
|
description = "A client to use a relay server."
|
||||||
|
authors = ["Tipragot <contact@tipragot.fr>"]
|
||||||
|
keywords = ["bevy", "network", "game"]
|
||||||
|
categories = ["network-programming", "game-development"]
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
tungstenite = { version = "0.21.0", features = ["rustls-tls-native-roots"] }
|
||||||
|
mio = { version = "0.8.10", features = ["net", "os-poll"] }
|
||||||
|
uuid = "1.7.0"
|
||||||
|
rand = "0.8.5"
|
||||||
|
home = "0.5.9"
|
||||||
|
log = "0.4.20"
|
361
crates/relay-client/src/lib.rs
Normal file
|
@ -0,0 +1,361 @@
|
||||||
|
//! A library containing a client to use a relay server.
|
||||||
|
|
||||||
|
use std::borrow::Cow;
|
||||||
|
use std::collections::LinkedList;
|
||||||
|
use std::fs;
|
||||||
|
use std::io::{self};
|
||||||
|
use std::net::{SocketAddr, ToSocketAddrs};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::sync::Mutex;
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
use log::warn;
|
||||||
|
use mio::net::TcpStream;
|
||||||
|
use rand::seq::SliceRandom;
|
||||||
|
use tungstenite::handshake::MidHandshake;
|
||||||
|
use tungstenite::stream::MaybeTlsStream;
|
||||||
|
use tungstenite::{ClientHandshake, HandshakeError, Message, WebSocket};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
/// The state of a [Connection].
|
||||||
|
#[derive(Debug)]
|
||||||
|
enum ConnectionState {
|
||||||
|
/// The [Connection] is not connected.
|
||||||
|
Disconnected,
|
||||||
|
|
||||||
|
/// The underlying [TcpStream] is connecting.
|
||||||
|
Connecting(TcpStream, Instant),
|
||||||
|
|
||||||
|
/// The underlying [TcpStream] is connected.
|
||||||
|
Connected(TcpStream),
|
||||||
|
|
||||||
|
/// The websocket handshake is in progress.
|
||||||
|
Handshaking(MidHandshake<ClientHandshake<MaybeTlsStream<TcpStream>>>),
|
||||||
|
|
||||||
|
/// The websocket handshake is finished.
|
||||||
|
Handshaked(WebSocket<MaybeTlsStream<TcpStream>>),
|
||||||
|
|
||||||
|
/// The [Connection] is registering with the relay server.
|
||||||
|
Registering(WebSocket<MaybeTlsStream<TcpStream>>),
|
||||||
|
|
||||||
|
/// The [Connection] is connected.
|
||||||
|
Active(WebSocket<MaybeTlsStream<TcpStream>>),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A connection to a relay server.
|
||||||
|
pub struct Connection {
|
||||||
|
/// The address list corresponding to the relay server.
|
||||||
|
address_list: Vec<SocketAddr>,
|
||||||
|
|
||||||
|
/// The domain of the relay server.
|
||||||
|
domain: String,
|
||||||
|
|
||||||
|
/// The path to the file where the identifier and secret key are stored.
|
||||||
|
data_path: PathBuf,
|
||||||
|
|
||||||
|
/// The identifier of the connection for the relay server.
|
||||||
|
identifier: Option<Uuid>,
|
||||||
|
|
||||||
|
/// The secret key used to authenticate with the relay server.
|
||||||
|
secret: Option<Uuid>,
|
||||||
|
|
||||||
|
/// A list of messages that needs to be sent.
|
||||||
|
to_send: Mutex<LinkedList<Message>>,
|
||||||
|
|
||||||
|
/// The state of the connection.
|
||||||
|
state: ConnectionState,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Connection {
|
||||||
|
/// Create a new [Connection].
|
||||||
|
pub fn new<'a>(domain: impl Into<Cow<'a, str>>) -> io::Result<Self> {
|
||||||
|
let domain = domain.into();
|
||||||
|
|
||||||
|
// Loads the identifier and secret key from disk.
|
||||||
|
let (data_path, identifier, secret) = {
|
||||||
|
// Find the relay data file path.
|
||||||
|
let mut path = home::home_dir().ok_or_else(|| {
|
||||||
|
io::Error::new(io::ErrorKind::NotFound, "could not find home directory")
|
||||||
|
})?;
|
||||||
|
path.push(".relay-data");
|
||||||
|
|
||||||
|
// Check if the file exists.
|
||||||
|
match false {
|
||||||
|
true => {
|
||||||
|
// Read the file and parse the identifier and secret key.
|
||||||
|
let contents = fs::read(&path)?;
|
||||||
|
if contents.len() != 32 {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidData,
|
||||||
|
"invalid data in .relay-data",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let identifier = Uuid::from_slice(&contents[..16]).map_err(io::Error::other)?;
|
||||||
|
let secret = Uuid::from_slice(&contents[16..]).map_err(io::Error::other)?;
|
||||||
|
(path, Some(identifier), Some(secret))
|
||||||
|
}
|
||||||
|
false => (path, None, None),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create the connection and return it.
|
||||||
|
Ok(Self {
|
||||||
|
address_list: (domain.as_ref(), 443).to_socket_addrs()?.collect(),
|
||||||
|
domain: domain.into_owned(),
|
||||||
|
data_path,
|
||||||
|
identifier,
|
||||||
|
secret,
|
||||||
|
to_send: Mutex::new(LinkedList::new()),
|
||||||
|
state: ConnectionState::Disconnected,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the identifier of the connection.
|
||||||
|
pub const fn identifier(&self) -> Option<Uuid> {
|
||||||
|
self.identifier
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send a message to the target client.
|
||||||
|
pub fn send<'a>(&self, target_id: Uuid, message: impl Into<Cow<'a, [u8]>>) {
|
||||||
|
let mut data = message.into().into_owned();
|
||||||
|
data.extend_from_slice(target_id.as_bytes());
|
||||||
|
if let Ok(mut to_send) = self.to_send.lock() {
|
||||||
|
to_send.push_back(Message::binary(data));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new [TcpStream] to the relay server.
|
||||||
|
fn create_stream(&mut self) -> ConnectionState {
|
||||||
|
// Take a random relay address.
|
||||||
|
let Some(address) = self.address_list.choose(&mut rand::thread_rng()) else {
|
||||||
|
warn!("no relay address available");
|
||||||
|
return ConnectionState::Disconnected;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create the new TCP stream.
|
||||||
|
match TcpStream::connect(address.to_owned()) {
|
||||||
|
Ok(stream) => ConnectionState::Connecting(stream, Instant::now()),
|
||||||
|
Err(e) => {
|
||||||
|
warn!("failed to start connection to the relay server: {e}");
|
||||||
|
ConnectionState::Disconnected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if the [TcpStream] of the [Connection] is connected.
|
||||||
|
fn check_connection(&mut self, stream: TcpStream, start: Instant) -> ConnectionState {
|
||||||
|
// Check for connection errors.
|
||||||
|
if let Err(e) = stream.take_error() {
|
||||||
|
warn!("failed to connect to the relay server: {e}");
|
||||||
|
return ConnectionState::Disconnected;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the stream is connected.
|
||||||
|
let connected = match stream.peek(&mut [0]) {
|
||||||
|
Ok(_) => true,
|
||||||
|
Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => true,
|
||||||
|
Err(ref e) if e.kind() == io::ErrorKind::NotConnected => false,
|
||||||
|
Err(e) => {
|
||||||
|
warn!("failed to connect to the relay server: {e}");
|
||||||
|
return ConnectionState::Disconnected;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if the connection has timed out.
|
||||||
|
let elapsed = start.elapsed();
|
||||||
|
if elapsed > Duration::from_secs(5) {
|
||||||
|
warn!("connection to the relay server timed out");
|
||||||
|
return ConnectionState::Disconnected;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the connection state if connected.
|
||||||
|
match connected {
|
||||||
|
true => ConnectionState::Connected(stream),
|
||||||
|
false => ConnectionState::Connecting(stream, start),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Start the websocket handshake.
|
||||||
|
fn start_handshake(&mut self, stream: TcpStream) -> ConnectionState {
|
||||||
|
match tungstenite::client_tls(format!("wss://{}", self.domain), stream) {
|
||||||
|
Ok((socket, _)) => ConnectionState::Handshaked(socket),
|
||||||
|
Err(HandshakeError::Interrupted(handshake)) => ConnectionState::Handshaking(handshake),
|
||||||
|
Err(HandshakeError::Failure(e)) => {
|
||||||
|
warn!("handshake failed with the relay server: {e}");
|
||||||
|
ConnectionState::Disconnected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Continue the websocket handshake.
|
||||||
|
fn continue_handshake(
|
||||||
|
&mut self,
|
||||||
|
handshake: MidHandshake<ClientHandshake<MaybeTlsStream<TcpStream>>>,
|
||||||
|
) -> ConnectionState {
|
||||||
|
match handshake.handshake() {
|
||||||
|
Ok((socket, _)) => ConnectionState::Handshaked(socket),
|
||||||
|
Err(HandshakeError::Interrupted(handshake)) => ConnectionState::Handshaking(handshake),
|
||||||
|
Err(HandshakeError::Failure(e)) => {
|
||||||
|
warn!("handshake failed with the relay server: {e}");
|
||||||
|
ConnectionState::Disconnected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Start authentication with the relay server.
|
||||||
|
fn start_authentication(
|
||||||
|
&mut self,
|
||||||
|
mut socket: WebSocket<MaybeTlsStream<TcpStream>>,
|
||||||
|
) -> ConnectionState {
|
||||||
|
match (self.identifier, self.secret) {
|
||||||
|
(Some(identifier), Some(secret)) => {
|
||||||
|
// Create the authentication message.
|
||||||
|
let mut data = Vec::with_capacity(32);
|
||||||
|
data.extend(identifier.as_bytes());
|
||||||
|
data.extend(secret.as_bytes());
|
||||||
|
|
||||||
|
// Send the authentication message.
|
||||||
|
match socket.send(Message::Binary(data)) {
|
||||||
|
Ok(()) => ConnectionState::Active(socket),
|
||||||
|
Err(e) => {
|
||||||
|
warn!("failed to send authentication message: {e}");
|
||||||
|
ConnectionState::Disconnected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
// Send empty authentication message to request a new identifier and secret key.
|
||||||
|
match socket.send(Message::Binary(vec![])) {
|
||||||
|
Ok(()) => ConnectionState::Registering(socket),
|
||||||
|
Err(e) => {
|
||||||
|
warn!("failed to send registration message: {e}");
|
||||||
|
ConnectionState::Disconnected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wait for the registration response.
|
||||||
|
fn get_registration_response(
|
||||||
|
&mut self,
|
||||||
|
mut socket: WebSocket<MaybeTlsStream<TcpStream>>,
|
||||||
|
) -> ConnectionState {
|
||||||
|
match socket.read() {
|
||||||
|
Ok(message) => {
|
||||||
|
// Check the message length.
|
||||||
|
let data = message.into_data();
|
||||||
|
if data.len() != 32 {
|
||||||
|
warn!("received malformed registration response");
|
||||||
|
return ConnectionState::Disconnected;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract the client identifier and secret.
|
||||||
|
self.identifier = Some(Uuid::from_slice(&data[..16]).expect("invalid identifier"));
|
||||||
|
self.secret = Some(Uuid::from_slice(&data[16..]).expect("invalid secret"));
|
||||||
|
|
||||||
|
// Save the client identifier and secret.
|
||||||
|
fs::write(&self.data_path, data).ok();
|
||||||
|
|
||||||
|
// Activate the connection.
|
||||||
|
ConnectionState::Active(socket)
|
||||||
|
}
|
||||||
|
Err(tungstenite::Error::Io(ref e))
|
||||||
|
if e.kind() == std::io::ErrorKind::WouldBlock
|
||||||
|
|| e.kind() == std::io::ErrorKind::Interrupted =>
|
||||||
|
{
|
||||||
|
ConnectionState::Registering(socket)
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
warn!("failed to receive registration response: {e}");
|
||||||
|
ConnectionState::Disconnected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update the [Connection] by receiving and sending messages.
|
||||||
|
fn update_connection(
|
||||||
|
&mut self,
|
||||||
|
mut socket: WebSocket<MaybeTlsStream<TcpStream>>,
|
||||||
|
messages: &mut LinkedList<(Uuid, Vec<u8>)>,
|
||||||
|
) -> ConnectionState {
|
||||||
|
// Unlock the sending list.
|
||||||
|
let Ok(mut to_send) = self.to_send.lock() else {
|
||||||
|
warn!("sending list closed");
|
||||||
|
return ConnectionState::Disconnected;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Send messages from the send channel to the socket.
|
||||||
|
while let Some(message) = to_send.pop_front() {
|
||||||
|
match socket.send(message) {
|
||||||
|
Ok(()) => (),
|
||||||
|
Err(tungstenite::Error::Io(ref e))
|
||||||
|
if e.kind() == std::io::ErrorKind::WouldBlock
|
||||||
|
|| e.kind() == std::io::ErrorKind::Interrupted =>
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
warn!("relay connection closed: {e}");
|
||||||
|
return ConnectionState::Disconnected;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Receive messages from the socket and send them to the receive channel.
|
||||||
|
loop {
|
||||||
|
match socket.read() {
|
||||||
|
Ok(message) => {
|
||||||
|
// Check the message length.
|
||||||
|
let mut data = message.into_data();
|
||||||
|
if data.len() < 16 {
|
||||||
|
warn!("received malformed message with length: {}", data.len());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract the sender ID.
|
||||||
|
let id_start = data.len() - 16;
|
||||||
|
let sender_id = Uuid::from_slice(&data[id_start..]).expect("invalid sender id");
|
||||||
|
data.truncate(id_start);
|
||||||
|
|
||||||
|
// Add the message to the message list.
|
||||||
|
messages.push_back((sender_id, data));
|
||||||
|
}
|
||||||
|
Err(tungstenite::Error::Io(ref e))
|
||||||
|
if e.kind() == std::io::ErrorKind::WouldBlock
|
||||||
|
|| e.kind() == std::io::ErrorKind::Interrupted =>
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
warn!("relay connection closed: {e}");
|
||||||
|
return ConnectionState::Disconnected;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep the connection connected.
|
||||||
|
ConnectionState::Active(socket)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update the [Connection] and return the received messages.
|
||||||
|
///
|
||||||
|
/// This function will connect to the relay server if it's not already
|
||||||
|
/// connected, and will send and receive messages from the relay server
|
||||||
|
/// if it's connected.
|
||||||
|
///
|
||||||
|
/// This function will not block the current thread.
|
||||||
|
pub fn update(&mut self) -> LinkedList<(Uuid, Vec<u8>)> {
|
||||||
|
let mut messages = LinkedList::new();
|
||||||
|
self.state = match std::mem::replace(&mut self.state, ConnectionState::Disconnected) {
|
||||||
|
ConnectionState::Disconnected => self.create_stream(),
|
||||||
|
ConnectionState::Connecting(stream, start) => self.check_connection(stream, start),
|
||||||
|
ConnectionState::Connected(stream) => self.start_handshake(stream),
|
||||||
|
ConnectionState::Handshaking(handshake) => self.continue_handshake(handshake),
|
||||||
|
ConnectionState::Handshaked(socket) => self.start_authentication(socket),
|
||||||
|
ConnectionState::Registering(socket) => self.get_registration_response(socket),
|
||||||
|
ConnectionState::Active(socket) => self.update_connection(socket, &mut messages),
|
||||||
|
};
|
||||||
|
messages
|
||||||
|
}
|
||||||
|
}
|
22
crates/relay-server/Cargo.toml
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
[package]
|
||||||
|
name = "relay-server"
|
||||||
|
version = "0.2.0"
|
||||||
|
edition = "2021"
|
||||||
|
license = "GPL-3.0-or-later"
|
||||||
|
description = "A relay server for bevnet."
|
||||||
|
authors = ["Tipragot <contact@tipragot.fr>"]
|
||||||
|
keywords = ["bevy", "network", "game"]
|
||||||
|
categories = ["network-programming", "game-development"]
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
tokio = { version = "1.36.0", features = ["macros", "rt-multi-thread"] }
|
||||||
|
axum = { version = "0.7.4", features = ["ws"] }
|
||||||
|
uuid = { version = "1.7.0", features = ["v4"] }
|
||||||
|
lazy_static = "1.4.0"
|
||||||
|
futures = "0.3.30"
|
||||||
|
dashmap = "5.5.3"
|
||||||
|
anyhow = "1.0.79"
|
||||||
|
sled = "0.34.7"
|
160
crates/relay-server/src/main.rs
Normal file
|
@ -0,0 +1,160 @@
|
||||||
|
//! A relay server for bevnet.
|
||||||
|
|
||||||
|
use std::io;
|
||||||
|
|
||||||
|
use anyhow::bail;
|
||||||
|
use axum::extract::ws::{Message, WebSocket};
|
||||||
|
use axum::extract::WebSocketUpgrade;
|
||||||
|
use axum::routing::get;
|
||||||
|
use axum::Router;
|
||||||
|
use dashmap::DashMap;
|
||||||
|
use futures::{SinkExt, StreamExt};
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
use sled::transaction::{ConflictableTransactionResult, TransactionalTree};
|
||||||
|
use sled::{Db, IVec};
|
||||||
|
use tokio::sync::mpsc::{channel, Receiver, Sender};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
static ref CLIENTS: DashMap<Uuid, Sender<Vec<u8>>> = DashMap::new();
|
||||||
|
static ref DB: Db = sled::open("/data/secrets.db").expect("unable to open the database");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
let app = Router::new().route(
|
||||||
|
"/",
|
||||||
|
get(|ws: WebSocketUpgrade| async {
|
||||||
|
ws.on_upgrade(|socket| async {
|
||||||
|
handle(socket).await.ok();
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
let listener = tokio::net::TcpListener::bind("0.0.0.0:80")
|
||||||
|
.await
|
||||||
|
.expect("failed to bind");
|
||||||
|
axum::serve(listener, app).await.expect("failed to serve");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new client and add it to the database.
|
||||||
|
fn create_client(tx: &TransactionalTree) -> ConflictableTransactionResult<(Uuid, Uuid), io::Error> {
|
||||||
|
// Generates a new identifier for the client.
|
||||||
|
let client_id = loop {
|
||||||
|
// Generates a new random identifier.
|
||||||
|
let id = Uuid::new_v4();
|
||||||
|
|
||||||
|
// Check if the id isn't already in the database.
|
||||||
|
if tx.get(id.as_bytes())?.is_none() {
|
||||||
|
break id;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Generate a random secret for the client.
|
||||||
|
let secret = Uuid::new_v4();
|
||||||
|
|
||||||
|
// Add the new client to the database.
|
||||||
|
tx.insert(client_id.as_bytes(), secret.as_bytes())?;
|
||||||
|
|
||||||
|
// Returns the client identifier and his secret.
|
||||||
|
Ok((client_id, secret))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle the websocket connection.
|
||||||
|
async fn handle(mut socket: WebSocket) -> anyhow::Result<()> {
|
||||||
|
// Receive the first request from the client.
|
||||||
|
let data = match socket.recv().await {
|
||||||
|
Some(Ok(message)) => message.into_data(),
|
||||||
|
_ => return Ok(()),
|
||||||
|
};
|
||||||
|
|
||||||
|
// If the request is empty it means that the client want a new identifier and
|
||||||
|
// secret, so we create them and send them to the client.
|
||||||
|
let client_id = if data.is_empty() {
|
||||||
|
// Generate the new client.
|
||||||
|
let (client_id, secret) = DB.transaction(create_client)?;
|
||||||
|
DB.flush_async().await?;
|
||||||
|
println!("{client_id} created");
|
||||||
|
|
||||||
|
// Send the data to the client.
|
||||||
|
let mut data = Vec::with_capacity(32);
|
||||||
|
data.extend_from_slice(client_id.as_bytes());
|
||||||
|
data.extend_from_slice(secret.as_bytes());
|
||||||
|
socket.send(Message::Binary(data)).await?;
|
||||||
|
|
||||||
|
// Returns the client identifier.
|
||||||
|
client_id
|
||||||
|
}
|
||||||
|
// Otherwise it means that the client want to reuse an identifier, so it will
|
||||||
|
// send it along with his secret to prove that he is the right client.
|
||||||
|
else {
|
||||||
|
// Check for the message length to detect malformed messages.
|
||||||
|
if data.len() != 32 {
|
||||||
|
bail!("malformed message");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the client identifier and secret from the message.
|
||||||
|
let client_id = Uuid::from_slice(&data[..16])?;
|
||||||
|
let secret = Uuid::from_slice(&data[16..])?;
|
||||||
|
|
||||||
|
// Check with the database if the secret is correct.
|
||||||
|
if DB.get(client_id.as_bytes())? != Some(IVec::from(secret.as_bytes())) {
|
||||||
|
bail!("invalid secret")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the client identifier.
|
||||||
|
client_id
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle the client connection.
|
||||||
|
println!("{client_id} connected");
|
||||||
|
let (sender, receiver) = channel(128);
|
||||||
|
CLIENTS.insert(client_id, sender);
|
||||||
|
handle_client(socket, client_id, receiver).await.ok();
|
||||||
|
CLIENTS.remove(&client_id);
|
||||||
|
println!("{client_id} disconnected");
|
||||||
|
|
||||||
|
// Returns success.
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle the client connection.
|
||||||
|
async fn handle_client(
|
||||||
|
socket: WebSocket,
|
||||||
|
client_id: Uuid,
|
||||||
|
mut receiver: Receiver<Vec<u8>>,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
// Split the socket into sender and receiver.
|
||||||
|
let (mut writer, mut reader) = socket.split();
|
||||||
|
|
||||||
|
// Handle sending messages to the client.
|
||||||
|
tokio::spawn(async move {
|
||||||
|
while let Some(message) = receiver.recv().await {
|
||||||
|
writer.send(Message::Binary(message)).await?;
|
||||||
|
}
|
||||||
|
Ok::<(), axum::Error>(())
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle messages from the client.
|
||||||
|
while let Some(Ok(message)) = reader.next().await {
|
||||||
|
// Get the target ID from the message.
|
||||||
|
let mut data = message.into_data();
|
||||||
|
if data.len() < 16 {
|
||||||
|
bail!("malformed message");
|
||||||
|
}
|
||||||
|
let id_start = data.len() - 16;
|
||||||
|
let target_id = Uuid::from_slice(&data[id_start..])?;
|
||||||
|
|
||||||
|
// Write the sender ID to the message.
|
||||||
|
for (i, &byte) in client_id.as_bytes().iter().enumerate() {
|
||||||
|
data[id_start + i] = byte;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send the message to the target client.
|
||||||
|
if let Some(sender) = CLIENTS.get(&target_id) {
|
||||||
|
sender.send(data).await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns success.
|
||||||
|
Ok(())
|
||||||
|
}
|
|
@ -1,21 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "server"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2021"
|
|
||||||
license = "GPL-3.0-or-later"
|
|
||||||
description = "The server of Border Wars"
|
|
||||||
repository = "https://git.tipragot.fr/corentin/border-wars.git"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
tokio = { version = "1.37.0", features = ["rt-multi-thread"] }
|
|
||||||
uuid = { version = "1.8.0", features = ["serde", "v7"] }
|
|
||||||
serde = { version = "1.0.197", features = ["derive"] }
|
|
||||||
axum = { version = "0.7.5", features = ["ws"] }
|
|
||||||
ascon-hash = "0.2.0"
|
|
||||||
rand = "0.8.5"
|
|
||||||
futures = "0.3.30"
|
|
||||||
bincode = "1.3.3"
|
|
||||||
lazy_static = "1.4.0"
|
|
||||||
|
|
||||||
# [lints]
|
|
||||||
# workspace = true
|
|
|
@ -1,5 +0,0 @@
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
|
||||||
pub struct JoinRequest(pub String, pub Option<Uuid>);
|
|
|
@ -1,13 +0,0 @@
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
pub struct LobbyManager {
|
|
||||||
connections: HashMap<Uuid, Uuid>,
|
|
||||||
lobbies: HashMap<Uuid, Lobby>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Lobby {
|
|
||||||
id: Uuid,
|
|
||||||
connections: HashMap<Uuid, Uuid>,
|
|
||||||
}
|
|
|
@ -1,112 +0,0 @@
|
||||||
use std::collections::hash_map::Entry;
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::convert::Infallible;
|
|
||||||
use std::sync::Arc;
|
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use ascon_hash::{AsconXof, ExtendableOutput, Update, XofReader};
|
|
||||||
use axum::extract::ws::{Message, WebSocket};
|
|
||||||
use axum::extract::{State, WebSocketUpgrade};
|
|
||||||
use axum::http::HeaderMap;
|
|
||||||
use axum::response::sse::{Event, KeepAlive};
|
|
||||||
use axum::response::{IntoResponse, Sse};
|
|
||||||
use axum::routing::get;
|
|
||||||
use axum::Router;
|
|
||||||
use futures::{SinkExt, StreamExt};
|
|
||||||
use server::JoinRequest;
|
|
||||||
use tokio::sync::mpsc::{channel, Sender};
|
|
||||||
use tokio::sync::RwLock;
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
mod lobby;
|
|
||||||
|
|
||||||
#[derive(Clone, Default)]
|
|
||||||
struct GameManager {
|
|
||||||
lobbies: Arc<RwLock<HashMap<Uuid, HashMap<Uuid, Sender<Vec<u8>>>>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::main]
|
|
||||||
async fn main() {
|
|
||||||
let app = Router::new()
|
|
||||||
.route("/ws", get(ws_handler))
|
|
||||||
.with_state(GameManager::default());
|
|
||||||
let listener = tokio::net::TcpListener::bind("0.0.0.0:80").await.unwrap();
|
|
||||||
axum::serve(listener, app).await.unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn ws_handler(
|
|
||||||
State(manager): State<GameManager>,
|
|
||||||
headers: HeaderMap,
|
|
||||||
ws: WebSocketUpgrade,
|
|
||||||
) -> impl IntoResponse {
|
|
||||||
ws.on_upgrade(|mut socket| async move {
|
|
||||||
// Handle authentication
|
|
||||||
let Some(Ok(message)) = socket.recv().await else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let mut reader = AsconXof::default()
|
|
||||||
.chain(message.into_data())
|
|
||||||
.finalize_xof();
|
|
||||||
let mut hash = [0u8; 16];
|
|
||||||
reader.read(&mut hash);
|
|
||||||
let id = Uuid::from_bytes(hash);
|
|
||||||
|
|
||||||
// Get client request
|
|
||||||
let Some(Ok(message)) = socket.recv().await else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let Ok(JoinRequest(username, lobby_id)) = bincode::deserialize(&message.into_data()) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Check if the client is in a game, if so, connect him to the game
|
|
||||||
todo!();
|
|
||||||
|
|
||||||
// Check if the client is already in a lobby, if so, refuse the connection
|
|
||||||
let mut lobbies = manager.lobbies.write().await;
|
|
||||||
for lobby in lobbies.values() {
|
|
||||||
if lobby.contains_key(&id) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find or create the lobby
|
|
||||||
let (lobby_id, lobby) = match lobby_id {
|
|
||||||
Some(id) if id == Uuid::nil() => lobbies
|
|
||||||
.iter_mut()
|
|
||||||
.min_by_key(|(_, lobby)| lobby.len())
|
|
||||||
.map(|(&id, lobby)| (id, lobby))
|
|
||||||
.unwrap_or_else(|| {
|
|
||||||
let id = Uuid::now_v7();
|
|
||||||
(id, lobbies.entry(id).or_default())
|
|
||||||
}),
|
|
||||||
Some(id) => {
|
|
||||||
let Some(lobby) = lobbies.get_mut(&id) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
(id, lobby)
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
let id = Uuid::now_v7();
|
|
||||||
(id, lobbies.entry(id).or_default())
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Initialize the sending loop
|
|
||||||
let (sender, mut receiver) = channel(1);
|
|
||||||
tokio::spawn(async move {
|
|
||||||
while let Some(message) = receiver.recv().await {
|
|
||||||
socket.send(Message::Binary(message)).await.ok();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Insert the client in the lobby
|
|
||||||
lobby.insert(id, sender);
|
|
||||||
drop(lobbies);
|
|
||||||
|
|
||||||
// Wait for the client to be ready
|
|
||||||
let Some(Ok(message)) = socket.recv().await else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -1,38 +0,0 @@
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
|
||||||
pub enum ClientPacket {
|
|
||||||
Disconnect,
|
|
||||||
CreateLobby {
|
|
||||||
username: String,
|
|
||||||
public: bool,
|
|
||||||
},
|
|
||||||
JoinLobby {
|
|
||||||
lobby_id: Option<Uuid>,
|
|
||||||
username: String,
|
|
||||||
},
|
|
||||||
IAmReady,
|
|
||||||
IAmNotReady,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
|
||||||
pub enum ServerPacket {
|
|
||||||
Refused(String),
|
|
||||||
LobbyJoined(Uuid),
|
|
||||||
LobbyUpdated(Lobby),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Serialize, Deserialize)]
|
|
||||||
pub struct Lobby {
|
|
||||||
pub public: bool,
|
|
||||||
pub players: HashMap<Uuid, LobbyPlayer>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Serialize, Deserialize)]
|
|
||||||
pub struct LobbyPlayer {
|
|
||||||
pub username: String,
|
|
||||||
pub ready: bool,
|
|
||||||
}
|
|
|
@ -1,18 +0,0 @@
|
||||||
use axum::http::header::Entry;
|
|
||||||
use dashmap::DashMap;
|
|
||||||
use lazy_static::lazy_static;
|
|
||||||
use scc::{HashMap, HashSet};
|
|
||||||
use server::LobbyStatus;
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
lazy_static! {
|
|
||||||
static ref LOBBIES: DashMap<Uuid, LobbyStatus> = DashMap::new();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn create_lobby(
|
|
||||||
client_id: Uuid,
|
|
||||||
username: String,
|
|
||||||
public: bool,
|
|
||||||
) -> anyhow::Result<()> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
|
@ -1,202 +0,0 @@
|
||||||
use std::borrow::{Borrow, Cow};
|
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use axum::extract::ws::{Message, WebSocket};
|
|
||||||
use axum::extract::WebSocketUpgrade;
|
|
||||||
use axum::routing::get;
|
|
||||||
use axum::Router;
|
|
||||||
use futures::{SinkExt, StreamExt};
|
|
||||||
use lazy_static::lazy_static;
|
|
||||||
use log::warn;
|
|
||||||
use server::{ClientPacket, Lobby, LobbyPlayer, ServerPacket};
|
|
||||||
use tokio::sync::mpsc::{channel, Sender};
|
|
||||||
use tokio::sync::RwLock;
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
struct Client {
|
|
||||||
status: ClientStatus,
|
|
||||||
sender: Sender<Vec<u8>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
enum ClientStatus {
|
|
||||||
Unauthenticated,
|
|
||||||
InLobby(Uuid),
|
|
||||||
InGame(Uuid),
|
|
||||||
}
|
|
||||||
|
|
||||||
lazy_static! {
|
|
||||||
static ref CLIENTS: RwLock<HashMap<Uuid, Client>> = RwLock::new(HashMap::new());
|
|
||||||
static ref LOBBIES: RwLock<HashMap<Uuid, Lobby>> = RwLock::new(HashMap::new());
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn send_message<'a>(client_id: Uuid, message: impl Into<Cow<'a, [u8]>>) {
|
|
||||||
if let Some(client) = CLIENTS.read().await.get(&client_id) {
|
|
||||||
client.sender.send(message.into().into_owned()).await.ok();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn send_packet(client_id: Uuid, packet: impl Borrow<ServerPacket>) {
|
|
||||||
let message = match bincode::serialize(packet.borrow()) {
|
|
||||||
Ok(message) => message,
|
|
||||||
Err(error) => {
|
|
||||||
warn!("failed to serialize packet for {}: {}", client_id, error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
send_message(client_id, message).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::main]
|
|
||||||
async fn main() {
|
|
||||||
let app = Router::new().route(
|
|
||||||
"/",
|
|
||||||
get(|ws: WebSocketUpgrade| async { ws.on_upgrade(handle_client) }),
|
|
||||||
);
|
|
||||||
let listener = tokio::net::TcpListener::bind("0.0.0.0:80")
|
|
||||||
.await
|
|
||||||
.expect("failed to bind");
|
|
||||||
axum::serve(listener, app).await.expect("failed to serve");
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handle_client(socket: WebSocket) {
|
|
||||||
let client_id = Uuid::now_v7();
|
|
||||||
|
|
||||||
let (mut sender, mut receiver) = socket.split();
|
|
||||||
let (send_tx, mut send_rx) = channel(16);
|
|
||||||
tokio::spawn(async move {
|
|
||||||
while let Some(message) = send_rx.recv().await {
|
|
||||||
sender.send(Message::Binary(message)).await?;
|
|
||||||
}
|
|
||||||
Ok::<(), axum::Error>(())
|
|
||||||
});
|
|
||||||
|
|
||||||
CLIENTS.write().await.insert(
|
|
||||||
client_id,
|
|
||||||
Client {
|
|
||||||
status: ClientStatus::Unauthenticated,
|
|
||||||
sender: send_tx,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
while let Some(Ok(message)) = receiver.next().await {
|
|
||||||
let Message::Binary(message) = message else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
let Ok(packet) = bincode::deserialize::<ClientPacket>(&message) else {
|
|
||||||
warn!("failed to deserialize packet from {}", client_id);
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
packet_received(client_id, packet).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
packet_received(client_id, ClientPacket::Disconnect).await;
|
|
||||||
CLIENTS.write().await.remove(&client_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn packet_received(client_id: Uuid, packet: ClientPacket) {
|
|
||||||
let client = &CLIENTS.read().await[&client_id];
|
|
||||||
match client.status {
|
|
||||||
ClientStatus::Unauthenticated => handle_unauthenticated(client_id, packet).await,
|
|
||||||
ClientStatus::InLobby(lobby_id) => handle_in_lobby(client_id, lobby_id, packet).await,
|
|
||||||
ClientStatus::InGame(game_id) => handle_in_game(client_id, game_id, packet).await,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handle_unauthenticated(client_id: Uuid, packet: ClientPacket) {
|
|
||||||
match packet {
|
|
||||||
ClientPacket::CreateLobby { username, public } => {
|
|
||||||
let lobby_id = Uuid::now_v7();
|
|
||||||
let lobby = Lobby {
|
|
||||||
public,
|
|
||||||
players: HashMap::from_iter([(
|
|
||||||
client_id,
|
|
||||||
LobbyPlayer {
|
|
||||||
username,
|
|
||||||
ready: false,
|
|
||||||
},
|
|
||||||
)]),
|
|
||||||
};
|
|
||||||
let message = bincode::serialize(&ServerPacket::LobbyUpdated(lobby.clone()))
|
|
||||||
.expect("failed to serialize lobby");
|
|
||||||
let mut lobbies = LOBBIES.write().await;
|
|
||||||
lobbies.insert(lobby_id, lobby);
|
|
||||||
CLIENTS
|
|
||||||
.write()
|
|
||||||
.await
|
|
||||||
.get_mut(&client_id)
|
|
||||||
.expect("client not found")
|
|
||||||
.status = ClientStatus::InLobby(lobby_id);
|
|
||||||
send_packet(client_id, ServerPacket::LobbyJoined(lobby_id)).await;
|
|
||||||
send_message(client_id, message).await;
|
|
||||||
}
|
|
||||||
ClientPacket::JoinLobby { lobby_id, username } => {
|
|
||||||
let mut lobbies = LOBBIES.write().await;
|
|
||||||
|
|
||||||
let (lobby_id, lobby) = match lobby_id {
|
|
||||||
Some(id) => {
|
|
||||||
let Some(lobby) = lobbies.get_mut(&id) else {
|
|
||||||
return send_packet(
|
|
||||||
client_id,
|
|
||||||
ServerPacket::Refused("lobby not found".to_string()),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
};
|
|
||||||
(id, lobby)
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
let random_lobby = lobbies
|
|
||||||
.iter_mut()
|
|
||||||
.filter(|(_, lobby)| lobby.public)
|
|
||||||
.min_by_key(|(_, lobby)| lobby.players.len());
|
|
||||||
match random_lobby {
|
|
||||||
Some((&id, lobby)) => (id, lobby),
|
|
||||||
None => {
|
|
||||||
let id = Uuid::now_v7();
|
|
||||||
(
|
|
||||||
id,
|
|
||||||
lobbies.entry(id).or_insert(Lobby {
|
|
||||||
public: true,
|
|
||||||
players: HashMap::new(),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
lobby.players.insert(
|
|
||||||
client_id,
|
|
||||||
LobbyPlayer {
|
|
||||||
username,
|
|
||||||
ready: false,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
CLIENTS
|
|
||||||
.write()
|
|
||||||
.await
|
|
||||||
.get_mut(&client_id)
|
|
||||||
.expect("client not found")
|
|
||||||
.status = ClientStatus::InLobby(lobby_id);
|
|
||||||
send_packet(client_id, ServerPacket::LobbyJoined(lobby_id)).await;
|
|
||||||
|
|
||||||
let message = bincode::serialize(&ServerPacket::LobbyUpdated(lobby.clone()))
|
|
||||||
.expect("failed to serialize lobby"); // PAS BON
|
|
||||||
for player_id in lobby.players.keys() {
|
|
||||||
send_message(*player_id, &message).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => (),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handle_in_lobby(client_id: Uuid, lobby_id: Uuid, packet: ClientPacket) {
|
|
||||||
match packet {
|
|
||||||
ClientPacket::Disconnect => todo!(),
|
|
||||||
ClientPacket::IAmReady => todo!(),
|
|
||||||
ClientPacket::IAmNotReady => todo!(),
|
|
||||||
_ => (),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handle_in_game(client_id: Uuid, game_id: Uuid, packet: ClientPacket) {}
|
|
|
@ -1,37 +0,0 @@
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
|
||||||
pub enum ClientMessage {
|
|
||||||
CreateLobby {
|
|
||||||
username: String,
|
|
||||||
public: bool,
|
|
||||||
},
|
|
||||||
JoinLobby {
|
|
||||||
username: String,
|
|
||||||
lobby_id: Option<Uuid>,
|
|
||||||
},
|
|
||||||
Ready(bool),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
|
||||||
pub enum ServerMessage {
|
|
||||||
Refused(String),
|
|
||||||
LobbyUpdate(Lobby),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Serialize, Deserialize)]
|
|
||||||
pub struct Lobby {
|
|
||||||
pub id: Uuid,
|
|
||||||
pub public: bool,
|
|
||||||
pub players: HashMap<Uuid, Player>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Serialize, Deserialize)]
|
|
||||||
pub struct Player {
|
|
||||||
pub id: Uuid,
|
|
||||||
pub username: String,
|
|
||||||
pub ready: bool,
|
|
||||||
}
|
|
|
@ -1,102 +0,0 @@
|
||||||
use std::borrow::Borrow;
|
|
||||||
use std::collections::hash_map::Entry;
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use ascon_hash::{AsconXof, ExtendableOutput, Update, XofReader};
|
|
||||||
use axum::extract::ws::Message;
|
|
||||||
use axum::extract::{State, WebSocketUpgrade};
|
|
||||||
use axum::response::IntoResponse;
|
|
||||||
use axum::routing::get;
|
|
||||||
use axum::Router;
|
|
||||||
use futures::{SinkExt, StreamExt};
|
|
||||||
use lazy_static::lazy_static;
|
|
||||||
use manager::handle_event;
|
|
||||||
use server::{ClientMessage, ServerMessage};
|
|
||||||
use tokio::sync::mpsc::{channel, Sender};
|
|
||||||
use tokio::sync::RwLock;
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
mod manager;
|
|
||||||
|
|
||||||
lazy_static! {
|
|
||||||
pub static ref CONNECTIONS: ConnectionManager = ConnectionManager::default();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct ConnectionManager(RwLock<HashMap<Uuid, Sender<Vec<u8>>>>);
|
|
||||||
|
|
||||||
impl ConnectionManager {
|
|
||||||
pub async fn send(&self, id: Uuid, message: impl Borrow<ServerMessage>) {
|
|
||||||
if let Some(sender) = self.0.read().await.get(&id) {
|
|
||||||
let Ok(message) = bincode::serialize(message.borrow()) else {
|
|
||||||
eprintln!("failed to serialize message");
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
sender.send(message).await.ok();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum ClientEvent {
|
|
||||||
Connected,
|
|
||||||
Message(ClientMessage),
|
|
||||||
Disconnected,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::main]
|
|
||||||
async fn main() {
|
|
||||||
let app = Router::new().route("/ws", get(ws_handler));
|
|
||||||
let listener = tokio::net::TcpListener::bind("0.0.0.0:80").await.unwrap();
|
|
||||||
axum::serve(listener, app).await.unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn ws_handler(ws: WebSocketUpgrade) -> impl IntoResponse {
|
|
||||||
ws.on_upgrade(|mut socket| async move {
|
|
||||||
// Handle authentication
|
|
||||||
let Some(Ok(message)) = socket.recv().await else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let mut reader = AsconXof::default()
|
|
||||||
.chain(message.into_data())
|
|
||||||
.finalize_xof();
|
|
||||||
let mut hash = [0u8; 16];
|
|
||||||
reader.read(&mut hash);
|
|
||||||
let id = Uuid::from_bytes(hash);
|
|
||||||
|
|
||||||
// Start the sending loop
|
|
||||||
let (mut writer, mut reader) = socket.split();
|
|
||||||
let (sender, mut receiver) = channel(1);
|
|
||||||
tokio::spawn(async move {
|
|
||||||
while let Some(message) = receiver.recv().await {
|
|
||||||
writer.send(Message::Binary(message)).await?;
|
|
||||||
}
|
|
||||||
Ok::<(), axum::Error>(())
|
|
||||||
});
|
|
||||||
|
|
||||||
// Register the client
|
|
||||||
match CONNECTIONS.0.write().await.entry(id) {
|
|
||||||
Entry::Occupied(_) => return,
|
|
||||||
Entry::Vacant(entry) => {
|
|
||||||
entry.insert(sender);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send the connection event
|
|
||||||
handle_event(id, ClientEvent::Connected).await;
|
|
||||||
|
|
||||||
// Handle incoming messages
|
|
||||||
while let Some(Ok(message)) = reader.next().await {
|
|
||||||
let Ok(message) = bincode::deserialize(&message.into_data()) else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
handle_event(id, ClientEvent::Message(message)).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send the disconnection event
|
|
||||||
handle_event(id, ClientEvent::Disconnected).await;
|
|
||||||
|
|
||||||
// Unregister the client
|
|
||||||
CONNECTIONS.0.write().await.remove(&id);
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -1,182 +0,0 @@
|
||||||
use std::collections::hash_map::{Entry, VacantEntry};
|
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use lazy_static::lazy_static;
|
|
||||||
use server::{Lobby, Player, ServerMessage};
|
|
||||||
use tokio::sync::mpsc::Sender;
|
|
||||||
use tokio::sync::{RwLock, RwLockWriteGuard};
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
use crate::{ClientEvent, ClientMessage, CONNECTIONS};
|
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
|
||||||
pub enum ClientStatus {
|
|
||||||
InLobby(Uuid),
|
|
||||||
InGame(Uuid),
|
|
||||||
}
|
|
||||||
|
|
||||||
lazy_static! {
|
|
||||||
pub static ref CLIENT_STATUS: RwLock<HashMap<Uuid, ClientStatus>> = RwLock::new(HashMap::new());
|
|
||||||
pub static ref LOBBIES: RwLock<HashMap<Uuid, Lobby>> = RwLock::new(HashMap::new());
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn handle_event(id: Uuid, event: ClientEvent) {
|
|
||||||
let status = CLIENT_STATUS.read().await.get(&id).copied();
|
|
||||||
match status {
|
|
||||||
None => handle_unauthenticated_event(id, event).await,
|
|
||||||
Some(ClientStatus::InLobby(lobby_id)) => handle_lobby_event(lobby_id, id, event).await,
|
|
||||||
Some(ClientStatus::InGame(game_id)) => handle_game_event(game_id, id, event).await,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handle_unauthenticated_event(id: Uuid, event: ClientEvent) {
|
|
||||||
match event {
|
|
||||||
ClientEvent::Message(ClientMessage::CreateLobby { username, public }) => {
|
|
||||||
// Create the lobby
|
|
||||||
let mut lobbies = LOBBIES.write().await;
|
|
||||||
let lobby_id = Uuid::now_v7();
|
|
||||||
let lobby = lobbies.entry(lobby_id).or_insert(Lobby {
|
|
||||||
id: lobby_id,
|
|
||||||
public,
|
|
||||||
players: HashMap::from_iter([(
|
|
||||||
id,
|
|
||||||
Player {
|
|
||||||
id,
|
|
||||||
username,
|
|
||||||
ready: false,
|
|
||||||
},
|
|
||||||
)]),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Change the client status
|
|
||||||
CLIENT_STATUS
|
|
||||||
.write()
|
|
||||||
.await
|
|
||||||
.insert(id, ClientStatus::InLobby(lobby.id));
|
|
||||||
|
|
||||||
// Send the lobby update
|
|
||||||
CONNECTIONS
|
|
||||||
.send(id, ServerMessage::LobbyUpdate(lobby.clone()))
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
ClientEvent::Message(ClientMessage::JoinLobby { username, lobby_id }) => {
|
|
||||||
// Find or create the lobby
|
|
||||||
let mut lobbies = LOBBIES.write().await;
|
|
||||||
let lobby = match lobby_id {
|
|
||||||
Some(lobby_id) => {
|
|
||||||
let Some(lobby) = lobbies.get_mut(&lobby_id) else {
|
|
||||||
CONNECTIONS
|
|
||||||
.send(id, ServerMessage::Refused("Lobby not found".to_string()))
|
|
||||||
.await;
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
lobby
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
if let Some((_, lobby)) = lobbies
|
|
||||||
.iter_mut()
|
|
||||||
.min_by_key(|(_, value)| value.players.len())
|
|
||||||
{
|
|
||||||
lobby
|
|
||||||
} else {
|
|
||||||
let lobby_id = Uuid::now_v7();
|
|
||||||
lobbies.entry(lobby_id).or_insert(Lobby {
|
|
||||||
id: lobby_id,
|
|
||||||
public: true,
|
|
||||||
players: HashMap::new(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Change the client status
|
|
||||||
CLIENT_STATUS
|
|
||||||
.write()
|
|
||||||
.await
|
|
||||||
.insert(id, ClientStatus::InLobby(lobby.id));
|
|
||||||
|
|
||||||
// Add the player to the lobby
|
|
||||||
lobby.players.insert(
|
|
||||||
id,
|
|
||||||
Player {
|
|
||||||
id,
|
|
||||||
username,
|
|
||||||
ready: false,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// Send the lobby update to all players
|
|
||||||
let message = ServerMessage::LobbyUpdate(lobby.clone());
|
|
||||||
for player_id in lobby.players.keys() {
|
|
||||||
CONNECTIONS.send(*player_id, &message).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => (),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handle_lobby_event(lobby_id: Uuid, id: Uuid, event: ClientEvent) {
|
|
||||||
match event {
|
|
||||||
ClientEvent::Message(ClientMessage::Ready(ready)) => {
|
|
||||||
// Get the lobby
|
|
||||||
let mut lobbies = LOBBIES.write().await;
|
|
||||||
let Some(lobby) = lobbies.get_mut(&lobby_id) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get the lobby player
|
|
||||||
let Some(player) = lobby.players.get_mut(&id) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Update the player ready status
|
|
||||||
player.ready = ready;
|
|
||||||
|
|
||||||
// If everyone is ready, start the game
|
|
||||||
if lobby.players.len() >= 2 && lobby.players.values().all(|p| p.ready) {
|
|
||||||
todo!("start the game");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send the lobby update to all players
|
|
||||||
let message = ServerMessage::LobbyUpdate(lobby.clone());
|
|
||||||
for player_id in lobby.players.keys() {
|
|
||||||
CONNECTIONS.send(*player_id, &message).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ClientEvent::Disconnected => {
|
|
||||||
// Remove the client status
|
|
||||||
CLIENT_STATUS.write().await.remove(&id);
|
|
||||||
|
|
||||||
// Remove the client from the lobby
|
|
||||||
let mut lobbies = LOBBIES.write().await;
|
|
||||||
let Some(lobby) = lobbies.get_mut(&lobby_id) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
lobby.players.remove(&id);
|
|
||||||
|
|
||||||
// If the lobby is empty, remove it
|
|
||||||
if lobby.players.is_empty() {
|
|
||||||
lobbies.remove(&lobby_id);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If everyone is ready, start the game
|
|
||||||
if lobby.players.len() >= 2 && lobby.players.values().all(|p| p.ready) {
|
|
||||||
todo!("start the game");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send the lobby update to all players
|
|
||||||
let message = ServerMessage::LobbyUpdate(lobby.clone());
|
|
||||||
for player_id in lobby.players.keys() {
|
|
||||||
CONNECTIONS.send(*player_id, &message).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => (),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handle_game_event(game_id: Uuid, id: Uuid, event: ClientEvent) {
|
|
||||||
todo!()
|
|
||||||
}
|
|