summaryrefslogtreecommitdiff
path: root/webhogg
diff options
context:
space:
mode:
authorDennis Kobert <d-kobert@web.de>2019-06-11 23:53:30 +0200
committerDennis Kobert <d-kobert@web.de>2019-06-11 23:53:30 +0200
commit3a3d0fc3d4733f8908e23a03f860d76340479ec4 (patch)
treecf4b82f61d01d2a24836e9820d73972436847982 /webhogg
parentc28c9fafa2c74b101f7ce777aac722dcdeecefc6 (diff)
Reorganize Project structure
Diffstat (limited to 'webhogg')
-rw-r--r--webhogg/game_server/Cargo.toml14
-rw-r--r--webhogg/game_server/src/backend_connection.rs90
-rw-r--r--webhogg/game_server/src/collide.rs238
-rw-r--r--webhogg/game_server/src/game_logger.rs30
-rw-r--r--webhogg/game_server/src/group.rs13
-rw-r--r--webhogg/game_server/src/lobby.rs63
-rw-r--r--webhogg/game_server/src/main.rs23
-rw-r--r--webhogg/game_server/src/maths.rs370
-rw-r--r--webhogg/game_server/src/scribble_group.rs65
-rw-r--r--webhogg/game_server/src/server.rs198
-rw-r--r--webhogg/game_server/src/webhogg_game.rs13
-rw-r--r--webhogg/game_server/src/webhogg_group.rs71
-rw-r--r--webhogg/game_server/src/webhogg_player.rs3
-rw-r--r--webhogg/game_server/src/ws_test.html74
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>