diff options
author | Dennis Kobert <d-kobert@web.de> | 2019-06-12 21:51:39 +0200 |
---|---|---|
committer | Dennis Kobert <d-kobert@web.de> | 2019-06-12 21:51:39 +0200 |
commit | 9e9c1c822a64c0a65033b7eed07ea661a385cecc (patch) | |
tree | ee699d4e93bb4204f5f4e04cd14f6d77365b81b4 /webhogg/game_server | |
parent | 304437b834e8c87687f68333ae67a13ee1377a73 (diff) | |
parent | 3a3d0fc3d4733f8908e23a03f860d76340479ec4 (diff) |
Merge remote-tracking branch 'origin/master'
Diffstat (limited to 'webhogg/game_server')
-rw-r--r-- | webhogg/game_server/Cargo.toml | 14 | ||||
-rw-r--r-- | webhogg/game_server/src/backend_connection.rs | 90 | ||||
-rw-r--r-- | webhogg/game_server/src/collide.rs | 238 | ||||
-rw-r--r-- | webhogg/game_server/src/game_logger.rs | 30 | ||||
-rw-r--r-- | webhogg/game_server/src/group.rs | 13 | ||||
-rw-r--r-- | webhogg/game_server/src/lobby.rs | 63 | ||||
-rw-r--r-- | webhogg/game_server/src/main.rs | 23 | ||||
-rw-r--r-- | webhogg/game_server/src/maths.rs | 370 | ||||
-rw-r--r-- | webhogg/game_server/src/scribble_group.rs | 65 | ||||
-rw-r--r-- | webhogg/game_server/src/server.rs | 198 | ||||
-rw-r--r-- | webhogg/game_server/src/webhogg_game.rs | 13 | ||||
-rw-r--r-- | webhogg/game_server/src/webhogg_group.rs | 71 | ||||
-rw-r--r-- | webhogg/game_server/src/webhogg_player.rs | 3 | ||||
-rw-r--r-- | webhogg/game_server/src/ws_test.html | 74 |
14 files changed, 1265 insertions, 0 deletions
diff --git a/webhogg/game_server/Cargo.toml b/webhogg/game_server/Cargo.toml new file mode 100644 index 0000000..bc14942 --- /dev/null +++ b/webhogg/game_server/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "game-server" +version = "0.1.0" +authors = ["natrixaeria", "truedoctor"] +edition = "2018" +description = "A general game server for connections to web clients. Currently (on the way to) deploying a skribbl.io like game." + +[dependencies] +log = "0.4" +fern = "0.5.8" +colored = "1.8" +reqwest = "0.9" +websocket = "0.22" +hyper = "0.10" diff --git a/webhogg/game_server/src/backend_connection.rs b/webhogg/game_server/src/backend_connection.rs new file mode 100644 index 0000000..a751b30 --- /dev/null +++ b/webhogg/game_server/src/backend_connection.rs @@ -0,0 +1,90 @@ +use reqwest::{Response, Client, Url, UrlError, Error as ReqError}; +use std::sync::mpsc::{Sender, Receiver}; +use std::sync::mpsc; +use crate::server::{UserId, Token}; +use crate::group::GroupId; + +pub struct BackendConnection { + host: String, + req_sender: Sender<RequestData>, + res_rec: Receiver<ResponseResult>, + max_uid: u32, +} + +#[derive(Debug)] +pub enum BackendError { + UrlError(UrlError), + RequestError(ReqError), + InvalidTokenFormat, + InvalidToken, + BadResponse(Response), +} + +pub type TokenValidity = Result<TokenResponse, BackendError>; +pub type RequestData = Url; +pub type ResponseResult = Result<Response, ReqError>; + +pub struct TokenResponse { + pub group_id: GroupId, + pub group_type: String, + pub group_name: String, + pub user_id: UserId, +} + +impl BackendConnection { + fn run_background(req_rec: Receiver<RequestData>, res_sender: Sender<ResponseResult>) { + let client = Client::new(); + loop { + let request_data = req_rec.recv().unwrap(); + let location = request_data; + let request = client.get(location); + let response = request.send(); + res_sender.send(response).unwrap(); + } + } + + pub fn new(host: &str) -> Self { + let (req_sender, req_rec): (Sender<RequestData>, Receiver<RequestData>) + = mpsc::channel(); + let (res_sender, res_rec): (Sender<ResponseResult>, Receiver<ResponseResult>) + = mpsc::channel(); + std::thread::spawn(move || Self::run_background(req_rec, res_sender)); + BackendConnection { + host: host.to_string(), + req_sender, + res_rec, + max_uid: 420, + } + } + + pub fn request(&self, location: &str) -> Result<(), UrlError> { + Ok(self.req_sender.send(Url::parse(&format!("{}{}", self.host, location))?).unwrap()) + } + + pub fn get_response(&self) -> ResponseResult { + self.res_rec.recv().unwrap() + } + + pub fn validate_token(&mut self, token: &Token) -> TokenValidity { + let location = format!("/api/lobby/tokens/{}", token); + self.request(&location).map_err(|err| BackendError::UrlError(err))?; + let response = self.get_response().map_err(|err| BackendError::RequestError(err))?; + if response.status().is_success() { + // zu Testzwecken werden noch keine JSON-Daten deserialisiert + // Dennis Server gibt ja noch nix zurück + self.max_uid += 1; + Ok(TokenResponse { + group_id: 12, + group_type: "scribble".to_string(), + group_name: "Scribble".to_string(), + user_id: self.max_uid - 1, + }) + } else if response.status() == reqwest::StatusCode::NOT_FOUND { + Err(BackendError::InvalidToken) + } else if response.status().is_client_error() { + Err(BackendError::InvalidTokenFormat) + } else { + Err(BackendError::BadResponse(response)) + } + } +} diff --git a/webhogg/game_server/src/collide.rs b/webhogg/game_server/src/collide.rs new file mode 100644 index 0000000..16b5357 --- /dev/null +++ b/webhogg/game_server/src/collide.rs @@ -0,0 +1,238 @@ +use crate::maths::{Vec2, AABox, RBox}; + +pub trait Collide<Rhs> { + fn collides(&self, other: &Rhs) -> bool; +} + +impl Collide<Vec2> for Vec2 { + fn collides(&self, other: &Self) -> bool { + self == other + } +} + +impl Collide<Vec2> for AABox { + fn collides(&self, other: &Vec2) -> bool { + self.pos < *other && other < &(self.pos + self.size) + } +} + +impl Collide<AABox> for AABox { + fn collides(&self, other: &Self) -> bool { + self.pos.x < other.pos.x + other.size.x && other.pos.x < self.pos.x + self.size.x + && self.pos.y < other.pos.y + other.size.y && other.pos.y < self.pos.y + self.size.y + } +} + +impl Collide<Vec2> for RBox { + fn collides(&self, other: &Vec2) -> bool { + let v1_diff = *other + self.v1 * (-self.v1.scalar(&(*other - self.pos)) / self.v1.distance2()); + let v2_diff = *other + self.v2 * (-self.v2.scalar(&(*other - self.pos)) / self.v2.distance2()); + + let v1_dist = ((v1_diff - self.pos) / self.v2).x; + let v2_dist = ((v2_diff - self.pos) / self.v1).x; + 0.0 <= v1_dist && v2_dist <= 1.0 + && 0.0 <= v2_dist && v2_dist <= 1.0 + //v1_diff < self.pos + self.v2 && self.pos < v1_diff + //&& v2_diff < self.pos + self.v1 && self.pos < v2_diff + } +} + +impl Collide<AABox> for RBox { + fn collides(&self, other: &AABox) -> bool { + let other_size = other.pos + other.size; + + // project points onto a orthogonal line + let v1_diff = other.pos + self.v1 * (-self.v1.scalar(&(other.pos - self.pos)) / self.v1.distance2()); + let v2_diff = other.pos + self.v2 * (-self.v2.scalar(&other.pos) / self.v2.distance2()); + let v1_diff_size = other_size + self.v1 * (-self.v1.scalar(&(other_size - self.pos)) / self.v1.distance2()); + let v2_diff_size = other_size + self.v2 * (-self.v2.scalar(&(other_size - self.pos)) / self.v2.distance2()); + + // calculate the distance + let v1_dist = ((v1_diff - self.pos) / self.v2); + let v2_dist = ((v2_diff - self.pos) / self.v1); + let v1_dist_size = ((v1_diff_size - self.pos) / self.v2); + let v2_dist_size = ((v2_diff_size - self.pos) / self.v1); + + let v1_dist = if v1_dist.x.is_finite() {v1_dist.x} else {v1_dist.y}; + let v2_dist = if v2_dist.x.is_finite() {v2_dist.x} else {v2_dist.y}; + let v1_dist_size = if v1_dist_size.x.is_finite() {v1_dist_size.x} else {v1_dist_size.y}; + let v2_dist_size = if v2_dist_size.x.is_finite() {v2_dist_size.x} else {v2_dist_size.y}; + + let minx = f32::min(self.pos.x, f32::min((self.pos + self.v1).x, (self.pos + self.v2).x)); + let maxx = f32::max(self.pos.x, f32::max((self.pos + self.v1).x, (self.pos + self.v2).x)); + let miny = f32::min(self.pos.y, f32::min((self.pos + self.v1).y, (self.pos + self.v2).y)); + let maxy = f32::max(self.pos.y, f32::max((self.pos + self.v1).y, (self.pos + self.v2).y)); + + 0.0 <= v1_dist_size && v1_dist <= 1.0 + && 0.0 <= v2_dist_size && v2_dist <= 1.0 + && other.pos.x <= maxx && minx <= other.pos.x + other.size.x + && other.pos.y <= maxy && miny <= other.pos.y + other.size.y + } +} + +impl<S, T: Collide<S>> Collide<S> for Vec<T> { + fn collides(&self, other: &S) -> bool { + self.iter().any(|x| x.collides(other)) + } +} + +#[cfg(test)] + mod tests { + use super::*; + + #[test] + fn test_collide_dot_dot() { + let a = Vec2{x: 1.0, y: 7.5}; + assert!(a.collides(&a)); + } + + #[test] + fn test_not_collide_dot_dot() { + let a = Vec2{x: 1.0, y: 7.5}; + let b = Vec2{x: 5.0, y: 7.5}; + assert!(!a.collides(&b)); + } + + #[test] + fn test_collide_aabox_dot() { + let a = Vec2{x: 1.0, y: 2.5}; + let b = Vec2{x: 3.0, y: 7.5}; + let c = Vec2{x: 1.5, y: 5.0}; + let aa_box = AABox{pos: a, size: b}; + + assert!(aa_box.collides(&c)); + } + + #[test] + fn test_not_collide_aabox_dot() { + let a = Vec2{x: 1.0, y: 7.5}; + let b = Vec2{x: 3.0, y: 2.5}; + let c = Vec2{x: 0.5, y: 5.0}; + let aa_box = AABox{pos: a, size: b}; + + assert!(!(aa_box.collides(&c))); + } + + #[test] + fn test_collide_aabox_aabox_intersecting() { + let a = Vec2{x: 1.0, y: 2.5}; + let b = Vec2{x: 3.0, y: 2.5}; + let aa_box = AABox{pos: a, size: b}; + let a = Vec2{x: 2.0, y: 3.5}; + let b = Vec2{x: 3.0, y: 7.5}; + let bb_box = AABox{pos: a, size: b}; + + assert!(aa_box.collides(&bb_box)); + } + + #[test] + fn test_collide_aabox_aabox_crossed() { + let a = Vec2{x: 2.0, y: 0.5}; + let b = Vec2{x: 1.0, y: 7.5}; + let aa_box = AABox{pos: a, size: b}; + let a = Vec2{x: 1.0, y: 3.5}; + let b = Vec2{x: 5.0, y: 4.5}; + let bb_box = AABox{pos: a, size: b}; + + assert!(aa_box.collides(&bb_box)); + } + + #[test] + fn test_not_collide_aabox_aabox() { + let a = Vec2{x: 1.0, y: 1.0}; + let b = Vec2{x: 1.0, y: 1.0}; + let aa_box = AABox{pos: a, size: b}; + let a = Vec2{x: 3.0, y: 3.5}; + let b = Vec2{x: 3.0, y: 7.5}; + let bb_box = AABox{pos: a, size: b}; + + assert!(!(aa_box.collides(&bb_box))); + } + + #[test] + fn test_collide_Rbox_dot() { + let a = Vec2{x: 1.0, y: 1.0}; + let b = Vec2{x: 1.0, y: 1.0}; + let c = Vec2{x: 1.0, y: -1.0}; + let aa_box = RBox{pos: a, v1: b, v2: c}; + + let c = Vec2{x: 1.6, y: 0.6}; + + assert!(aa_box.collides(&c)); + } + + #[test] + fn test_not_collide_rbox_dot() { + let a = Vec2{x: 1.0, y: 1.0}; + let b = Vec2{x: 1.0, y: 1.0}; + let c = Vec2{x: 1.0, y: -1.0}; + let aa_box = RBox{pos: a, v1: b, v2: c}; + + let c = Vec2{x: 1.4, y: 0.4}; + + assert!(!(aa_box.collides(&c))); + } + + #[test] + fn test_collide_rbox_aabox_intersecting() { + let a = Vec2{x: 1.0, y: 2.5}; + let b = Vec2{x: 0.0, y: 2.5}; + let c = Vec2{x: 3.0, y: 0.5}; + let aa_box = RBox{pos: a, v1: b, v2: c}; + let a = Vec2{x: 2.0, y: 3.5}; + let b = Vec2{x: 3.0, y: 7.5}; + let bb_box = AABox{pos: a, size: b}; + + assert!(aa_box.collides(&bb_box)); + } + + #[test] + fn test_collide_rbox_aabox_edges_touch() { + let a = Vec2{x: 4.0, y: 5.5}; + let b = Vec2{x: 1.0, y: 7.5}; + let aa_box = RBox::new(a, b, 3.9); + let a = Vec2{x: 0.0, y: 0.5}; + let b = Vec2{x: 4.0, y: 5.0}; + let bb_box = AABox{pos: a, size: b}; + + assert!(aa_box.collides(&bb_box)); + } + + #[test] + fn test_collide_rbox_aabox_crossed() { + let a = Vec2{x: 2.0, y: 0.5}; + let b = Vec2{x: 1.0, y: 7.5}; + let aa_box = RBox::new(a, b, 3.9); + let a = Vec2{x: 0.0, y: 4.5}; + let b = Vec2{x: 15.0, y: 1.5}; + let bb_box = AABox{pos: a, size: b}; + + assert!(aa_box.collides(&bb_box)); + } + + #[test] + fn test_not_collide_rbox_aabox_next_to() { + let a = Vec2{x: 2.0, y: 0.5}; + let b = Vec2{x: 1.0, y: 7.5}; + let aa_box = RBox::new(a, b, 3.9); + let a = Vec2{x: 5.0, y: 40.5}; + let b = Vec2{x: 15.0, y: 1.5}; + let bb_box = AABox{pos: a, size: b}; + + assert!(!aa_box.collides(&bb_box)); + } + + #[test] + fn test_not_collide_rbox_aabox() { + let a = Vec2{x: 1.0, y: 1.0}; + let b = Vec2{x: 0.0, y: 1.0}; + let c = Vec2{x: 1.0, y: 0.0}; + let aa_box = RBox{pos: a, v1: b, v2: c}; + let a = Vec2{x: 3.0, y: 3.5}; + let b = Vec2{x: 3.0, y: 7.5}; + let bb_box = AABox{pos: a, size: b}; + + assert!(!(aa_box.collides(&bb_box))); + } + +} diff --git a/webhogg/game_server/src/game_logger.rs b/webhogg/game_server/src/game_logger.rs new file mode 100644 index 0000000..c51bfc5 --- /dev/null +++ b/webhogg/game_server/src/game_logger.rs @@ -0,0 +1,30 @@ +use colored::*; + +fn color_level(level: log::Level) -> colored::ColoredString { + let text = format!("{: <8}", level); + match level { + log::Level::Error => text.red().bold(), + log::Level::Warn => text.yellow(), + log::Level::Info => text.green(), + log::Level::Debug => text.cyan(), + log::Level::Trace => text.magenta(), + } +} + +pub fn init_logger() { + fern::Dispatch::new().format(|out, message, record|{ + out.finish(format_args!( + "{} {} > {}", + color_level(record.level()), + record.target(), + message + ) + ) + }) + .level(log::LevelFilter::Debug) + .level_for("hyper", log::LevelFilter::Off) + .level_for("tokio_reactor", log::LevelFilter::Off) + .level_for("reqwest", log::LevelFilter::Off) + .chain(std::io::stdout()) + .apply().unwrap(); +} diff --git a/webhogg/game_server/src/group.rs b/webhogg/game_server/src/group.rs new file mode 100644 index 0000000..74a04f7 --- /dev/null +++ b/webhogg/game_server/src/group.rs @@ -0,0 +1,13 @@ +use crate::server::{UserId, GameClient, GameServerError}; + +pub type GroupId = u32; + +pub trait Group { + fn id(&self) -> GroupId; + fn group_type(&self) -> String; + fn name(&self) -> String; + + fn run(&mut self); + + fn add_client(&mut self, id: UserId, client: GameClient) -> Result<(), GameServerError>; +} diff --git a/webhogg/game_server/src/lobby.rs b/webhogg/game_server/src/lobby.rs new file mode 100644 index 0000000..6d11a5f --- /dev/null +++ b/webhogg/game_server/src/lobby.rs @@ -0,0 +1,63 @@ +use std::collections::HashMap; + +use crate::group::{Group, GroupId}; +use crate::scribble_group::ScribbleGroup; + +use crate::server::{UserId, GameClient, GameServerError}; + +pub struct Lobby { + groups: HashMap<GroupId, Box<Group>>, +} + +#[allow(dead_code)] +impl Lobby { + pub fn new() -> Self { + Self { + groups: HashMap::new(), + } + } + + fn generate_group(group_type: &str, id: GroupId, name: &str) -> Option<Box<Group>> { + match group_type { + "scribble" => { + Some(Box::new(ScribbleGroup::new(id, name.to_string()))) + }, + _ => None, + } + } + + pub fn add_group(&mut self, group: Box<Group>) { + self.groups.insert(group.id(), group); + } + + pub fn add_client(&mut self, group_type: &str, group_id: GroupId, group_name: &str, + user_id: UserId, client: GameClient) -> Result<(), GameServerError> { + if !self.groups.contains_key(&group_id) { + let mut group = match Self::generate_group(group_type, group_id, group_name) { + Some(x) => x, + _ => return Err(GameServerError::GroupCreationError(format!("failed to generate '{}' group", group_type))), + }; + group.run(); + self.groups.insert(group_id, group); + } + let group = self.groups.get_mut(&group_id).unwrap(); + group.add_client(user_id, client) + } + + pub fn iter<'b>(&'b self) -> GroupIterator<'b> { + GroupIterator { groups: self.groups.values() } + } +} + +#[allow(dead_code)] +pub struct GroupIterator<'a> { + groups: std::collections::hash_map::Values<'a, GroupId, Box<Group>> +} + +impl<'a> Iterator for GroupIterator<'a> { + type Item = &'a Box<Group>; + + fn next(&mut self) -> Option<Self::Item> { + self.groups.next() + } +} diff --git a/webhogg/game_server/src/main.rs b/webhogg/game_server/src/main.rs new file mode 100644 index 0000000..cfd9787 --- /dev/null +++ b/webhogg/game_server/src/main.rs @@ -0,0 +1,23 @@ +mod group; +mod maths; +mod scribble_group; +mod webhogg_group; +mod webhogg_game; +mod collide; +mod lobby; +mod server; +mod backend_connection; + +mod game_logger; + +#[macro_use] extern crate log; + +fn main() { + game_logger::init_logger(); + + let addr = ("0.0.0.0", 5001); + info!("create game server on {:?}", addr); + let mut gameserver = server::GameServer::new(addr); + gameserver.run().unwrap(); + +} diff --git a/webhogg/game_server/src/maths.rs b/webhogg/game_server/src/maths.rs new file mode 100644 index 0000000..b9303af --- /dev/null +++ b/webhogg/game_server/src/maths.rs @@ -0,0 +1,370 @@ +#[derive(Clone, Copy, Debug)] +pub struct Vec2 { + pub x: f32, + pub y: f32, +} + +impl std::ops::Add for Vec2 { + type Output = Self; + fn add(self, other: Self) -> Self { + Self { + x: self.x + other.x, + y: self.y + other.y + } + } +} + +impl std::ops::AddAssign<Vec2> for Vec2 { + fn add_assign(&mut self, other: Vec2) { + self.x += other.x; + self.y += other.y; + } +} + +impl std::ops::Sub for Vec2 { + type Output = Self; + fn sub(self, other: Self) -> Self { + Self { + x: self.x - other.x, + y: self.y - other.y + } + } +} + +impl std::ops::SubAssign<Vec2> for Vec2 { + fn sub_assign(&mut self, other: Vec2) { + self.x -= other.x; + self.y -= other.y; + } +} + +impl std::ops::Neg for Vec2 { + type Output = Self; + fn neg(self) -> Self { + Self { + x: -self.x, + y: -self.y + } + } +} + +impl std::ops::Mul<f32> for Vec2 { + type Output = Self; + fn mul(self, scale: f32) -> Self { + Self { + x: self.x * scale, + y: self.y * scale + } + } +} + +impl std::ops::Div<f32> for Vec2 { + type Output = Self; + fn div(self, scale: f32) -> Self { + Self { + x: self.x / scale, + y: self.y / scale + } + } +} + +impl std::ops::Div<Vec2> for Vec2 { + type Output = Self; + fn div(self, scale: Vec2) -> Self { + Self { + x: self.x / scale.x, + y: self.y / scale.y + } + } +} + + +impl std::cmp::PartialOrd for Vec2 { + fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> { + if self.x <= other.x && self.y <= other.y { + Some(std::cmp::Ordering::Less) + } else if self.x > other.x && self.y > other.y { + Some(std::cmp::Ordering::Greater) + } else { + None + } + } +} + +impl std::cmp::PartialEq for Vec2 { + fn eq(&self, other: &Self) -> bool { + f32::abs(self.x - other.x) < 1e-8 + && f32::abs(self.y - other.y) < 1e-8 + } +} + +impl std::cmp::Eq for Vec2 {} + +impl Vec2 { + pub fn distance(&self) -> f32 { + f32::sqrt(self.distance2()) + } + + pub fn distance2(&self) -> f32 { + self.scalar(self) + } + + pub fn scalar(&self, other: &Vec2) -> f32 { + self.x * other.x + self.y * other.y + } + + pub fn norm(&self) -> Vec2 { + let len = self.distance(); + Vec2 { + x: self.x / len, + y: self.y / len, + } + } +} + +#[derive(Clone, Copy, Debug)] +pub struct AABox { + pub pos: Vec2, + /// the size may not be smaller than zero + pub size: Vec2, +} + +impl std::ops::Add<Vec2> for AABox { + type Output = Self; + fn add(self, other: Vec2) -> Self { + Self { + pos: self.pos + other, + size: self.size, + } + } +} + +impl std::ops::AddAssign<Vec2> for AABox { + fn add_assign(&mut self, other: Vec2) { + self.pos += other + } +} + +impl std::ops::Sub<Vec2> for AABox { + type Output = Self; + fn sub(self, other: Vec2) -> Self { + Self { + pos: self.pos + other, + size: self.size + } + } +} + +impl std::ops::SubAssign<Vec2> for AABox { + fn sub_assign(&mut self, other: Vec2) { + self.pos -= other + } +} + +impl std::cmp::PartialEq for AABox { + fn eq(&self, other: &Self) -> bool { + self.pos == other.pos + && self.size == other.size + } +} + +impl std::cmp::Eq for AABox {} + +#[derive(Clone, Copy, Debug)] +pub struct RBox { + /// origin + pub pos: Vec2, + /// Vwctor1 + pub v1: Vec2, + /// Vector2 + pub v2: Vec2, +} + +impl RBox { + pub fn new(pos: Vec2, orientation: Vec2, width: f32) -> Self { + let scale = width / orientation.distance(); + let orth = Vec2 {x: orientation.x / scale, y: -orientation.y / scale}; + Self { + pos: pos, + v1: orientation, + v2: orth, + } + } +} + +impl std::ops::Add<Vec2> for RBox { + type Output = Self; + fn add(self, other: Vec2) -> Self { + Self { + pos: self.pos + other, + v1: self.v1, + v2: self.v2, + } + } +} + +impl std::ops::AddAssign<Vec2> for RBox { + fn add_assign(&mut self, other: Vec2) { + self.pos += other + } +} + +impl std::ops::Sub<Vec2> for RBox { + type Output = Self; + fn sub(self, other: Vec2) -> Self { + Self { + pos: self.pos + other, + v1: self.v1 + other, + v2: self.v2, + } + } +} + +impl std::ops::SubAssign<Vec2> for RBox { + fn sub_assign(&mut self, other: Vec2) { + self.pos -= other + } +} + +impl std::cmp::PartialEq for RBox { + fn eq(&self, other: &Self) -> bool { + self.pos == other.pos + && self.v1 == other.v1 + && self.v1 == self.v2 + } +} + +impl std::cmp::Eq for RBox {} + +#[cfg(test)] +mod tests { + // Note this useful idiom: importing names from outer (for mod tests) scope. + use super::*; + + #[test] + fn test_less_vec2() { + let a = Vec2{x: 1.0, y: 7.5}; + let b = Vec2{x: -3.0, y: 2.5}; + + assert!(b < a); + } + + #[test] + fn test_less_vec2_fail() { + let a = Vec2{x: 1.0, y: 7.5}; + let b = Vec2{x: 3.0, y: 2.5}; + + assert!(!(a < b)); + } + + #[test] + fn test_greater_vec2() { + let a = Vec2{x: 1.0, y: 7.5}; + let b = Vec2{x: -3.0, y: 2.5}; + + assert!(a > b); + } + + #[test] + fn test_greater_vec2_fail() { + let a = Vec2{x: 1.0, y: 7.5}; + let b = Vec2{x: 3.0, y: 2.5}; + + assert!(!(a > b)); + } + + + #[test] + fn test_add_vec2() { + let a = Vec2{x: 1.0, y: 7.5}; + let b = Vec2{x: -3.0, y: 2.5}; + let c = Vec2{x: -2.0, y: 10.0}; + + assert_eq!(a + b, c); + } + + #[test] + fn test_neg_vec2() { + let a = Vec2{x: 1.0, y: 7.5}; + let b = Vec2{x: -1.0, y: -7.5}; + + assert_eq!(-a, b); + } + + #[test] + fn test_sub_vec2() { + let a = Vec2{x: 1.0, y: 7.5}; + let b = Vec2{x: -3.0, y: 2.5}; + let c = Vec2{x: 4.0, y: 5.0}; + + assert_eq!(a - b, c); + } + + #[test] + fn test_distance_vec2() { + let a = Vec2{x: 2.0, y: 2.0}; + + assert!(f32::abs(a.distance() - 2.0) < 1e8); + } + + #[test] + fn test_distance2_vec2() { + let a = Vec2{x: 1.0, y: 2.0}; + + assert!(f32::abs(a.distance2() - 5.0) < 1e8); + } + + #[test] + fn test_norm_vec2() { + let a = Vec2{x: 2.0, y: -2.0}; + let b = Vec2{x: std::f32::consts::FRAC_1_SQRT_2, y: -std::f32::consts::FRAC_1_SQRT_2}; + + assert_eq!(a.norm(), b); + } + + #[test] + fn test_add_aabox_vec2() { + let a = Vec2{x: 1.0, y: 7.5}; + let b = Vec2{x: -3.0, y: 2.5}; + let mut aa_box = AABox{pos: a, size: b}; + let bb_box = AABox{pos: a + b,size: b}; + aa_box += b; + + assert_eq!(aa_box, bb_box); + } + + #[test] + fn test_sub_aabox_vec2() { + let a = Vec2{x: 1.0, y: 7.5}; + let b = Vec2{x: -3.0, y: 2.5}; + let mut aa_box = AABox{pos: a, size: b}; + let bb_box = AABox{pos: a - b,size: b}; + aa_box -= b; + + assert_eq!(aa_box, bb_box); + } + + #[test] + fn test_add_rbox_vec2() { + let a = Vec2{x: 1.0, y: 7.5}; + let b = Vec2{x: -3.0, y: 2.5}; + let c = Vec2{x: -3.0, y: 2.5}; + let mut aa_box = RBox{pos: a, v1: b, v2: c}; + let bb_box = RBox{pos: a + b, v1: b, v2: c}; + aa_box += b; + + assert_eq!(aa_box, bb_box); + } + + #[test] + fn test_sub_rbox_vec2() { + let a = Vec2{x: 1.0, y: 7.5}; + let b = Vec2{x: -3.0, y: 2.5}; + let c = Vec2{x: -3.0, y: 2.5}; + let mut aa_box = RBox{pos: a, v1: b, v2: c}; + let bb_box = RBox{pos: a - b, v1: b, v2: c}; + aa_box -= b; + + assert_eq!(aa_box, bb_box); + } +} diff --git a/webhogg/game_server/src/scribble_group.rs b/webhogg/game_server/src/scribble_group.rs new file mode 100644 index 0000000..8980e7b --- /dev/null +++ b/webhogg/game_server/src/scribble_group.rs @@ -0,0 +1,65 @@ +use crate::group::{Group, GroupId}; +use crate::server::{UserId, GameClient, + ClientSender, ClientReceiver, + GameServerError}; +use std::collections::HashMap; +use std::sync::{Arc, Mutex}; + +pub struct ScribbleGroup { + id: GroupId, + name: String, + senders: Arc<Mutex<HashMap<UserId, ClientSender>>> +} + +impl Group for ScribbleGroup { + fn id(&self) -> GroupId { + self.id + } + + fn group_type(&self) -> String { + "scribble".to_string() + } + + fn name(&self) -> String { + self.name.clone() + } + + fn run(&mut self) { + info!("a new group {}:'{}' runs now", self.id, self.name); + } + + fn add_client(&mut self, id: UserId, client: GameClient) -> Result<(), GameServerError> { + debug!("user {} joined the group {}:'{}'", id, self.id, self.name); + let (sen, rec) = client.split(); + self.senders.lock().unwrap().insert(id, sen); + let senders_mutex = self.senders.clone(); + let self_uid = id; + std::thread::spawn(move || Self::broadcast_clients(self_uid, rec, senders_mutex)); + Ok(()) + } +} + +impl ScribbleGroup { + pub fn new(id: GroupId, name: String) -> Self { + Self { id, name, senders: Arc::new(Mutex::new(HashMap::new())) } + } + + fn broadcast_clients(self_uid: UserId, mut rec: ClientReceiver, senders_mutex: Arc<Mutex<HashMap<UserId, ClientSender>>>) { + loop { + let message = match rec.recv_message() { + Ok(x) => x, + _ => break + }; + //trace!("got message: '{:?}'", message); + let mut senders = senders_mutex.lock().unwrap(); + for (uid, sender) in senders.iter_mut() { + if self_uid != *uid { + sender.send_message(&message) + .unwrap_or_else(|_| debug!("tried to send message to {}, but failed", *uid)); + } + } + } + senders_mutex.lock().unwrap().remove(&self_uid); + info!("client {} has left", self_uid); + } +} diff --git a/webhogg/game_server/src/server.rs b/webhogg/game_server/src/server.rs new file mode 100644 index 0000000..5b1a7a9 --- /dev/null +++ b/webhogg/game_server/src/server.rs @@ -0,0 +1,198 @@ +use websocket::{OwnedMessage, + stream::sync::Splittable, + sync::Server, + client::sync::Client, + server::{NoTlsAcceptor, + sync::AcceptResult}, + receiver, sender}; +use std::net::{SocketAddr, ToSocketAddrs, TcpStream}; +use std::sync::{mpsc, + mpsc::{Sender, Receiver}}; +use crate::lobby::Lobby; +use crate::backend_connection::BackendConnection; + +pub type ClientReceiver = receiver::Reader<<TcpStream as Splittable>::Reader>; +pub type ClientSender = sender::Writer<<TcpStream as Splittable>::Writer>; + +const PROTOCOL: &str = "tuesday"; + +pub type Token = u32; +pub type UserId = u32; + +#[derive(Debug)] +pub enum GameServerError { + BindError(std::io::Error), + HandshakeRequestError, + InvalidProtocolError, + AcceptError(std::io::Error), + GroupError(String), + GroupCreationError(String), +} + +impl std::fmt::Display for GameServerError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { + match self { + GameServerError::BindError(e) => write!(f, "BindError: {}", e), + GameServerError::HandshakeRequestError => write!(f, "HandshakeRequestError"), + GameServerError::InvalidProtocolError => write!(f, "InvalidProtocolError"), + GameServerError::AcceptError(e) => write!(f, "AcceptError: {}", e), + GameServerError::GroupError(e) => write!(f, "GroupError: {}", e), + GameServerError::GroupCreationError(e) => write!(f, "GroupCreationError: {}", e), + } + } +} + +pub struct GameServer { + addr: SocketAddr, + lobby: Lobby, + backend: BackendConnection, +} + +pub struct GameClient { + addr: SocketAddr, + client: Client<TcpStream>, +} + +impl GameClient { + fn from_raw(client: Client<TcpStream>) -> Result<Self, ()> { + let addr = client.peer_addr().map_err(|_| ())?; + info!("got a client connection from: {}", addr); + Ok(GameClient { + addr, + client, + }) + } + + fn require_token(&mut self) -> Option<Token> { + let message = self.client + .recv_message() + .ok()?; + if let OwnedMessage::Text(text) = message { + text.parse().ok() + } else { + None + } + } + + fn host_name(&self) -> SocketAddr { + self.addr + } + + pub fn split(self) -> (ClientSender, ClientReceiver) { + let (rec, sen) = self.client.split().unwrap(); + (sen, rec) + } +} + +type ClientConnection = Result<GameClient, GameServerError>; + +impl GameServer { + pub fn new<T: ToSocketAddrs>(addr: T) -> Self { + let addr = addr.to_socket_addrs().unwrap().next().unwrap(); + debug!("ws address: {}", addr); + info!("create lobby"); + let lobby = Lobby::new(); + let backend = BackendConnection::new("https://kobert.dev"); + info!("got a C# backend connection"); + GameServer { + addr, + lobby: lobby, + backend: backend, + } + } + + pub fn run(&mut self) -> Result<(), GameServerError> { + let reader = self.read_clients(); + loop { + let connection = reader.recv().unwrap()?; + self.add_client(connection); + } + } + + fn add_client(&mut self, mut client: GameClient) { + let token = client.require_token(); + if let Some(token) = token { + let result = self.backend.validate_token(&token); + match result { + Err(err) => warn!("client's token {} is not valid: '{:?}'", + token, err), + Ok(result) => { + debug!("client validation was successfull"); + let user_id = result.user_id; + let group_id = result.group_id; + let group_type = result.group_type; + let group_name = result.group_name; + debug!("add client: (id:{}, token:{}, host:{}) to \"{}\"", + user_id, token, client.host_name(), group_name); + //clients.lock().unwrap().insert(token, client); + self.lobby.add_client(&group_type, group_id, + &group_name, user_id, client) + .unwrap_or_else(|e| warn!("failed to add client: {}", e)); + } + } + } else { + warn!("client sent invalid token"); + } + } + + fn read_clients(&self) -> Receiver<ClientConnection> { + let (sen, rec): (Sender<ClientConnection>, Receiver<ClientConnection>) + = mpsc::channel(); + let addr = self.addr; + std::thread::spawn(move || { + match Self::handle_requests(addr, &sen) { + Err(e) => sen.send(Err(e)).unwrap(), + _ => (), + } + }); + rec + } + + fn handle_requests(addr: SocketAddr, sen: &Sender<ClientConnection>) -> Result<(), GameServerError> { + let server = match Server::<NoTlsAcceptor>::bind(addr) { + Ok(v) => v, + Err(e) => { + error!("websocket binding error"); + Err(GameServerError::BindError(e))? + }, + }; + info!("webserver is being launched"); + for req in server { + sen.send(Ok(Self::handle_request(req)?)).unwrap(); + } + info!("webserver is being shut down"); + Ok(()) + } + + fn handle_request(req: AcceptResult<TcpStream>) -> ClientConnection { + match req { + Ok(req) => { + if !req.protocols().contains(&PROTOCOL.to_string()) { + warn!("a client tried to connect without {} protocol", PROTOCOL); + req.reject().unwrap(); + Err(GameServerError::InvalidProtocolError) + } else { + match req.use_protocol(PROTOCOL).accept() { + Ok(client) => { + match GameClient::from_raw(client) { + Ok(client) => Ok(client), + Err(_) => { + error!("could not create a client"); + Err(GameServerError::HandshakeRequestError) + } + } + }, + Err((_, e)) => { + warn!("client handshake failed"); + Err(GameServerError::AcceptError(e)) + } + } + } + }, + Err(_) => { + warn!("invalid client request"); + Err(GameServerError::HandshakeRequestError) + } + } + } +} diff --git a/webhogg/game_server/src/webhogg_game.rs b/webhogg/game_server/src/webhogg_game.rs new file mode 100644 index 0000000..7b94fcb --- /dev/null +++ b/webhogg/game_server/src/webhogg_game.rs @@ -0,0 +1,13 @@ +use crate::maths::Vec2; + +pub struct WebhoggPlayer { + pos: Vec2, +} + +pub struct WebhoggGame { + player1: WebhoggPlayer, + player2: WebhoggPlayer, +} + +impl WebhoggGame { +} diff --git a/webhogg/game_server/src/webhogg_group.rs b/webhogg/game_server/src/webhogg_group.rs new file mode 100644 index 0000000..091f7f8 --- /dev/null +++ b/webhogg/game_server/src/webhogg_group.rs @@ -0,0 +1,71 @@ +use crate::group::{Group, GroupId}; +use crate::server::{UserId, GameClient, + ClientSender, ClientReceiver, + GameServerError}; +use std::collections::HashMap; +use std::sync::{Arc, Mutex}; + +pub struct WebhoggGroup { + id: GroupId, + name: String, + senders: Arc<Mutex<HashMap<UserId, ClientSender>>> +} + +impl Group for WebhoggGroup { + fn id(&self) -> GroupId { + self.id + } + + fn group_type(&self) -> String { + "webhogg".to_string() + } + + fn name(&self) -> String { + self.name.clone() + } + + fn run(&mut self) { + info!("a new group {}:'{}' runs now", self.id, self.name); + } + + fn add_client(&mut self, id: UserId, client: GameClient) -> Result<(), GameServerError> { + if self.senders.lock().unwrap().len() > 1 { + return Err(GameServerError::GroupError( + format!("user {} was not able to join the {} group, {}", + "because the client limit has been exceeded", + id, self.name))); + } + debug!("user {} joined the group {}:'{}'", id, self.id, self.name); + let (sen, rec) = client.split(); + self.senders.lock().unwrap().insert(id, sen); + let senders_mutex = self.senders.clone(); + let self_uid = id; + std::thread::spawn(move || Self::broadcast_clients(self_uid, rec, senders_mutex)); + Ok(()) + } +} + +impl WebhoggGroup { + pub fn new(id: GroupId, name: String) -> Self { + Self { id, name, senders: Arc::new(Mutex::new(HashMap::new())) } + } + + fn broadcast_clients(self_uid: UserId, mut rec: ClientReceiver, senders_mutex: Arc<Mutex<HashMap<UserId, ClientSender>>>) { + loop { + let message = match rec.recv_message() { + Ok(x) => x, + _ => break + }; + //trace!("got message: '{:?}'", message); + let mut senders = senders_mutex.lock().unwrap(); + for (uid, sender) in senders.iter_mut() { + if self_uid != *uid { + sender.send_message(&message) + .unwrap_or_else(|_| debug!("tried to send message to {}, but failed", *uid)); + } + } + } + senders_mutex.lock().unwrap().remove(&self_uid); + info!("client {} has left", self_uid); + } +} diff --git a/webhogg/game_server/src/webhogg_player.rs b/webhogg/game_server/src/webhogg_player.rs new file mode 100644 index 0000000..38b9596 --- /dev/null +++ b/webhogg/game_server/src/webhogg_player.rs @@ -0,0 +1,3 @@ +pub struct WebhoggPlayer { + +} diff --git a/webhogg/game_server/src/ws_test.html b/webhogg/game_server/src/ws_test.html new file mode 100644 index 0000000..3b3d4ce --- /dev/null +++ b/webhogg/game_server/src/ws_test.html @@ -0,0 +1,74 @@ +<!doctype html> +<html> + <head> + <title>WS Test</title> + <style> +.b { + border-bottom: 1px solid black; +} + </style> + </head> + <body style='background: black; color: white'> + <div id='cons'>connected</div><br> + <button onclick='test_connection()'>Launch</button><br> + <span>Server address: </span><input id='addr'></input> + <div align='right'><span>Message</span><input id='msg'></input> <button onclick='send_text()'>Send</button></div> + <div id='chat' style='background: rgb(20, 20, 20); padding-left: 20px; margin: 40px' /> + </body> + <script> +function get_addr() { + return document.getElementById('addr').value; +} + +function test_connection() { + let a = 'ws://' + get_addr(); + add_text('create a new connection at "' + a + '"'); + ws = new WebSocket(a, 'tuesday'); + ws.addEventListener('open', function (event) { + add_text('connection established'); + toggle_connected(true); + // send token + ws.send('42'); + }); + ws.addEventListener('error', function (event) { + add_text('ws error occured: "' + event + '"'); + toggle_connected(false); + }); + ws.addEventListener('close', function (event) { + add_text('ws is closed now'); + toggle_connected(false); + }); + ws.addEventListener('message', function (event) { + add_text('got ws message: "' + event.data + '"'); + }); +} + +function send_text() { + let msg = document.getElementById('msg').value; + ws.send(msg); + add_text('sent message: "' + msg + '"'); +} + +function add_text(text, color='white') { + let c = document.getElementById('chat'); + let n = document.createElement('span'); + n.setAttribute('class', 'b'); + n.style = 'color: ' + color; + n.textContent = (new Date()).toTimeString().substring(0, 8) + '|> '+ text; + c.appendChild(n); + c.appendChild(document.createElement('br')); +} +function toggle_connected(con) { + let c = document.getElementById('cons'); + if (con) { + c.style = 'background: green' + c.textContent = 'connected'; + } else { + c.style = 'background: red' + c.textContent = 'not connected'; + } +} +toggle_connected(false); +add_text("JS loaded"); + </script> +</html> |