Nicolas Winkler 4 роки тому
батько
коміт
c25eebbdb0
5 змінених файлів з 350 додано та 132 видалено
  1. 16 6
      src/game.rs
  2. 5 2
      src/main.rs
  3. 260 0
      src/server.rs
  4. 12 109
      src/websocket.rs
  5. 57 15
      static/index.html

+ 16 - 6
src/game.rs

@@ -1,31 +1,37 @@
 use std::time::{Duration, Instant};
 use serde::{Serialize, Deserialize};
+use actix::{Actor, Addr, Context};
+use crate::websocket;
+use crate::server;
 
-#[derive(Serialize, Deserialize)]
-#[serde(rename_all = "lowercase")]
+#[derive(Serialize, Deserialize, Clone)]
 pub enum GameState {
     Creating,
     Guessing(usize),
 }
 
-#[derive(Serialize, Deserialize)]
+#[derive(Serialize, Deserialize, Clone)]
 pub struct CreatingEx {
     pub question: String,
     pub letters: Vec<char>
 }
 
-#[derive(Serialize, Deserialize)]
+#[derive(Serialize, Deserialize, Clone)]
 pub struct Round {
     pub exercises: Vec<CreatingEx>
 }
 
-#[derive(Serialize, Deserialize)]
+#[derive(Serialize, Clone)]
 pub struct Player {
     nick: String,
+
+    #[serde(skip_serializing)]
+    actor: Addr<server::GameConnection>,
+
     submitted_word: Option<String>,
 }
 
-#[derive(Serialize, Deserialize)]
+#[derive(Serialize, Clone)]
 pub struct Game {
     players: Vec<Player>,
     round_number: i32,
@@ -81,5 +87,9 @@ impl Game {
             }
         }
     }
+
+    pub fn player_join(&mut self, nick: &str, actor: Addr<server::GameConnection>) {
+        self.players.push(Player{ nick: nick.to_owned(), actor, submitted_word: None });
+    }
 }
 

+ 5 - 2
src/main.rs

@@ -1,10 +1,12 @@
 use std::collections::BTreeMap;
 use std::sync::Mutex;
+use actix::prelude::*;
 use actix_files as fs;
 use actix_web::{middleware, web, App, HttpServer};
 
 mod websocket;
 mod game;
+mod server;
 //use crate::websocket::ws_initiate;
 
 
@@ -13,11 +15,12 @@ async fn main() -> std::io::Result<()> {
     std::env::set_var("RUST_LOG", "actix_server=info,actix_web=info");
     env_logger::init();
 
-    let mut games_list = web::Data::new(Mutex::new(BTreeMap::<String, game::Game>::new()));
+    let mut server = server::Server { lobbies: BTreeMap::new() };
+    let server_addr = server.start();
 
     HttpServer::new(move || {
         App::new()
-            .app_data(games_list.clone())
+            .app_data(web::Data::new(server_addr.clone()))
             // enable logger
             .wrap(middleware::Logger::default())
             // websocket route

+ 260 - 0
src/server.rs

@@ -0,0 +1,260 @@
+use std::collections::BTreeMap;
+use std::collections::BTreeSet;
+use std::time::{Duration, Instant};
+use std::sync::{Mutex, RwLock, Arc};
+use actix::dev::{MessageResponse, ResponseChannel};
+use actix::prelude::*;
+use actix_web::{web, Error, HttpRequest, HttpResponse};
+use actix_web_actors::ws;
+use futures::executor::block_on;
+use crate::game;
+use crate::websocket::{ClientMessage, UpdateMessage};
+
+const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(5);
+const CLIENT_TIMEOUT: Duration = Duration::from_secs(10);
+
+#[derive(Message)]
+#[rtype(result = "Answer")]
+struct JoinRequest{ lobby_id: String, p: Addr<GameConnection> }
+
+
+#[derive(Message)]
+#[rtype(result = "Answer")]
+struct CreateLobbyRequest{ lobby_id: String, p: Addr<GameConnection> }
+
+#[derive(Message)]
+#[rtype(result = "game::Game")]
+struct GetGame;
+
+enum Answer {
+    LobbyJoined(Addr<GameLobby>),
+    LobbyCreated(Addr<GameLobby>),
+    LobbyAlreadyExists,
+    NoSuchLobby,
+}
+
+
+impl<A, M> MessageResponse<A, M> for Answer
+where
+    A: Actor,
+    M: Message<Result = Answer>,
+{
+    fn handle<R: ResponseChannel<M>>(self, _: &mut A::Context, tx: Option<R>) {
+        if let Some(tx) = tx {
+            tx.send(self);
+        }
+    }
+}
+
+
+impl<A, M> MessageResponse<A, M> for game::Game
+where
+    A: Actor,
+    M: Message<Result = game::Game>,
+{
+    fn handle<R: ResponseChannel<M>>(self, _: &mut A::Context, tx: Option<R>) {
+        if let Some(tx) = tx {
+            tx.send(self);
+        }
+    }
+}
+
+pub struct Server {
+    pub lobbies: BTreeMap<String, Addr<GameLobby>>,
+}
+
+impl Actor for Server {
+    type Context = Context<Self>;
+    fn started(&mut self, ctx: &mut Self::Context) {
+    }
+}
+
+impl Handler<JoinRequest> for Server {
+    type Result = Answer;
+    fn handle(&mut self, jr: JoinRequest, ctx: &mut Self::Context) -> Self::Result {
+        let mb_lobby = self.lobbies.get(&jr.lobby_id);
+        match mb_lobby {
+            Some(lobby) => {
+                let _sent = lobby.send(jr);
+                Answer::LobbyJoined(lobby.clone())
+            },
+            None => Answer::NoSuchLobby
+        }
+    }
+}
+
+impl Handler<CreateLobbyRequest> for Server {
+    type Result = Answer;
+    fn handle(&mut self, clr: CreateLobbyRequest, ctx: &mut Self::Context) -> Self::Result {
+        let existing_lobby = self.lobbies.get(&clr.lobby_id);
+        match existing_lobby {
+            Some(_) => Answer::LobbyAlreadyExists,
+            None => {
+                let lobby = GameLobby::new(clr.lobby_id.clone());
+                let lobby_addr = lobby.start();
+                self.lobbies.insert(clr.lobby_id, lobby_addr.clone());
+                Answer::LobbyCreated(lobby_addr)
+            }
+        }
+    }
+}
+
+pub struct GameLobby {
+    connected_players: Vec<Addr<GameConnection>>,
+    game_id: String,
+    game: game::Game,
+}
+
+impl Actor for GameLobby {
+    type Context = Context<Self>;
+    fn started(&mut self, ctx: &mut Self::Context) {
+    }
+}
+
+impl Handler<JoinRequest> for GameLobby {
+    type Result = Answer;
+    fn handle(&mut self, jr: JoinRequest, ctx: &mut Self::Context) -> Self::Result {
+        self.connected_players.push(jr.p);
+        Answer::NoSuchLobby
+    }
+}
+
+impl Handler<GetGame> for GameLobby {
+    type Result = game::Game;
+    fn handle(&mut self, gg: GetGame, ctx: &mut Self::Context) -> Self::Result {
+        self.game.clone()
+    }
+}
+
+impl GameLobby {
+    pub fn new(gi: String) -> Self {
+        GameLobby {
+            connected_players: vec![],
+            game_id: gi,
+            game: game::Game::new()
+        }
+    }
+}
+
+///
+/// connection to one single client
+/// 
+pub struct GameConnection {
+    heartbeat: Instant,
+    nick: Option<String>,
+    game_id: Option<String>,
+    server: Addr<Server>,
+    game_lobby: Option<Addr<GameLobby>>
+}
+
+impl Actor for GameConnection {
+    type Context = ws::WebsocketContext<Self>;
+
+    fn started(&mut self, ctx: &mut Self::Context) {
+        self.initiate_heartbeat(ctx);
+    }
+}
+
+impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for GameConnection {
+    fn handle(
+        &mut self,
+        msg: Result<ws::Message, ws::ProtocolError>,
+        ctx: &mut Self::Context,
+    ) {
+        // process websocket messages
+        match msg {
+            Ok(ws::Message::Ping(msg)) => {
+                self.heartbeat = Instant::now();
+                ctx.pong(&msg);
+            }
+            Ok(ws::Message::Pong(_)) => {
+                self.heartbeat = Instant::now();
+            }
+            Ok(ws::Message::Text(text)) => {
+                println!("hmmm: {:?}", text);
+                self.handle_message(&text, ctx);
+            },
+            Ok(ws::Message::Binary(bin)) => ctx.binary(bin),
+            Ok(ws::Message::Close(reason)) => {
+                ctx.close(reason);
+                ctx.stop();
+            }
+            _ => ctx.stop(),
+        }
+    }
+}
+
+impl GameConnection {
+    pub fn new(server_addr: Addr<Server>) -> Self {
+        GameConnection {
+            heartbeat: Instant::now(),
+            nick: None,
+            game_id: None,
+            server: server_addr,
+            game_lobby: None
+        }
+    }
+
+    pub fn initiate_heartbeat(&self, ctx: &mut <Self as Actor>::Context) {
+        ctx.run_interval(HEARTBEAT_INTERVAL, |act, ctx| {
+            if Instant::now().duration_since(act.heartbeat) > CLIENT_TIMEOUT {
+                //println!("Websocket Client heartbeat failed, disconnecting!");
+                ctx.stop();
+                return;
+            }
+            ctx.ping(b"");
+        });
+    }
+
+    pub fn send_message(&self, m: &UpdateMessage, ctx: &mut <Self as Actor>::Context) {
+        let txt = serde_json::to_string(m).unwrap();
+        println!("{:?}", txt);
+        ctx.text(txt);
+    }
+
+    pub fn handle_message(&mut self, text: &str, ctx: &mut <Self as Actor>::Context) {
+        println!("hm: {:?}", text);
+        let parsed: Result<ClientMessage, _> = serde_json::from_str(text);
+        if let Ok(msg) = parsed {
+            match msg {
+                ClientMessage::CreateGame{game_id, nick} => {
+                    self.game_id = Some(game_id.clone());
+                    self.nick = Some(nick);
+                    let lobby_addr = block_on(self.server.send(CreateLobbyRequest{ lobby_id: game_id.clone(), p: ctx.address() }));
+                    match lobby_addr {
+                        Ok(Answer::LobbyAlreadyExists) => {
+                            self.send_message(&UpdateMessage::GameAlreadyExists{ game_id: game_id.clone() }, ctx);
+                        },
+                        Ok(Answer::LobbyCreated(lobby_addr)) => {
+                            let gm = block_on(lobby_addr.send(GetGame));
+                            self.send_message(&UpdateMessage::GameState(gm.unwrap()), ctx);
+                        },
+                        _ => {
+                            println!("internal error creating lobby");
+                        }
+                    }
+                },
+                ClientMessage::Join{game_id, nick} => {
+                    self.game_id = Some(game_id.clone());
+                    self.nick = Some(nick);
+                    let lobby_addr = block_on(self.server.send(JoinRequest{ lobby_id: game_id.clone(), p: ctx.address() }));
+                    match lobby_addr {
+                        Ok(Answer::NoSuchLobby) => {
+                            self.send_message(&UpdateMessage::GameNotFound{ game_id: game_id.clone() }, ctx);
+                        },
+                        Ok(Answer::LobbyJoined(lobby_addr)) => {
+                            let gm = block_on(lobby_addr.send(GetGame));
+                            self.send_message(&UpdateMessage::GameState(gm.unwrap()), ctx);
+                        },
+                        _ => {
+                            println!("internal error joining lobby");
+                        }
+                    }
+                },
+            }
+        }
+        else {
+            println!("error parsing json");
+        }
+    }
+}

+ 12 - 109
src/websocket.rs

@@ -1,130 +1,33 @@
-use std::time::{Duration, Instant};
-use std::collections::BTreeMap;
-use std::sync::Mutex;
-
 use serde::{Serialize, Deserialize};
 
 use actix::prelude::*;
+use actix::Message;
 use actix_web::{web, Error, HttpRequest, HttpResponse};
 use actix_web_actors::ws;
 
 use crate::game;
+use crate::server;
 
-const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(5);
-const CLIENT_TIMEOUT: Duration = Duration::from_secs(10);
-
-pub async fn ws_initiate(map: web::Data<Mutex<BTreeMap<String, game::Game>>>, r: HttpRequest, stream: web::Payload) -> Result<HttpResponse, Error> {
+pub async fn ws_initiate(server: web::Data<Addr<server::Server>>, r: HttpRequest, stream: web::Payload) -> Result<HttpResponse, Error> {
     println!("{:?}", r);
-    let res = ws::start(WebSock::new(map.clone()), &r, stream);
+    let (addr, res) = ws::start_with_addr(server::GameConnection::new(server.as_ref().clone()), &r, stream)?;
     println!("{:?}", res);
-    res
+    Ok(res)
 }
 
 #[derive(Serialize, Deserialize)]
-#[serde(rename_all = "lowercase")]
-enum Message {
+#[serde(rename_all = "snake_case")]
+pub enum ClientMessage {
+    CreateGame{ game_id: String, nick: String },
     Join{ game_id: String, nick: String },
 }
 
 #[derive(Serialize)]
-#[serde(rename_all = "lowercase")]
-enum UpdateMessage<'a> {
+#[serde(rename_all = "snake_case")]
+pub enum UpdateMessage {
     GameNotFound{ game_id: String },
-    GameState(&'a game::Game)
-}
-
-struct WebSock {
-    heartbeat: Instant,
-    nick: Option<String>,
-    game_id: Option<String>,
-    server_data: web::Data<Mutex<BTreeMap<String, game::Game>>>,
-}
-
-impl Actor for WebSock {
-    type Context = ws::WebsocketContext<Self>;
-
-    fn started(&mut self, ctx: &mut Self::Context) {
-        self.initiate_heartbeat(ctx);
-    }
+    GameAlreadyExists{ game_id: String },
+    GameState(game::Game)
 }
 
-impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for WebSock {
-    fn handle(
-        &mut self,
-        msg: Result<ws::Message, ws::ProtocolError>,
-        ctx: &mut Self::Context,
-    ) {
-        // process websocket messages
-        println!("WS: {:?}", msg);
-        match msg {
-            Ok(ws::Message::Ping(msg)) => {
-                self.heartbeat = Instant::now();
-                ctx.pong(&msg);
-            }
-            Ok(ws::Message::Pong(_)) => {
-                self.heartbeat = Instant::now();
-            }
-            Ok(ws::Message::Text(text)) => {
-                self.handle_message(&text, ctx);
-            },
-            Ok(ws::Message::Binary(bin)) => ctx.binary(bin),
-            Ok(ws::Message::Close(reason)) => {
-                ctx.close(reason);
-                ctx.stop();
-            }
-            _ => ctx.stop(),
-        }
-    }
-}
-
-impl WebSock {
-    pub fn new(map: web::Data<Mutex<BTreeMap<String, game::Game>>>) -> Self {
-        Self { heartbeat: Instant::now(), nick: None, game_id: None, server_data: map }
-    }
-
-    pub fn initiate_heartbeat(&self, ctx: &mut <Self as Actor>::Context) {
-        ctx.run_interval(HEARTBEAT_INTERVAL, |act, ctx| {
-            if Instant::now().duration_since(act.heartbeat) > CLIENT_TIMEOUT {
-                //println!("Websocket Client heartbeat failed, disconnecting!");
-                ctx.stop();
-                return;
-            }
-            ctx.ping(b"");
-        });
-    }
-
-    pub fn send_message(&self, m: &UpdateMessage, ctx: &mut <WebSock as Actor>::Context) {
-        let txt = serde_json::to_string(m).unwrap();
-        println!("{:?}", txt);
-        ctx.text(txt);
-    }
 
-    pub fn handle_message(&mut self, text: &str, ctx: &mut <WebSock as Actor>::Context) {
-        let x = Message::Join{ game_id: "gmae".to_owned(), nick: "name".to_owned() };
-        let string = serde_json::to_string(&x);
-        println!("{}", string.unwrap());
-
-
-        let parsed: Result<Message, _> = serde_json::from_str(text);
-        if let Ok(msg) = parsed {
-            match msg {
-                Message::Join{game_id, nick} => {
-                    println!("just joined: {}, {}", game_id, nick);
-                    self.game_id = Some(game_id.clone());
-                    self.nick = Some(nick);
-                    let mut map = self.server_data.lock().unwrap();
-                    match map.get(&game_id) {
-                        Some(game) => {
-                            self.send_message(&UpdateMessage::GameState(game), ctx);
-                        },
-                        None => {
-                            self.send_message(&UpdateMessage::GameNotFound{ game_id: game_id.clone() }, ctx);
-                            map.insert(game_id, game::Game::new());
-                            println!("game not found");
-                        }
-                    }
-                },
-            }
-        }
-    }
-}

+ 57 - 15
static/index.html

@@ -6,34 +6,73 @@
     <script language="javascript" type="text/javascript">
         $(function() {
             var connection = null;
+
             $('#join').click(function() {
+                var game_id = $('#gameId').val();
+                var nick = $('#nick').val();
+                var msg = {
+                    join: {
+                        game_id: game_id,
+                        nick: nick,
+                    }
+                };
+                connect(msg);
+            });
+
+            $('#create').click(function() {
+                var game_id = $('#gameId').val();
+                var nick = $('#nick').val();
+                var msg = {
+                    create_game: {
+                        game_id: game_id,
+                        nick: nick,
+                    }
+                };
+                connect(msg);
+            });
+
+            function connect(initial_message) {
                 if (connection != null) {
-                    connection.close();
-                    connection = null;
+                    disconnect();
                 }
                 var uri = 'ws://' + window.location.host + '/ws/';
                 connection = new WebSocket(uri);
-                
-                var game_id = $('#gameId').val();
-                var nick = $('#nick').val();
 
                 connection.onopen = function() {
-                    var msg = {
-                        join: {
-                            game_id: game_id,
-                            nick: nick,
-                        }
-                    };
-                    connection.send(JSON.stringify(msg));
+                    connection.send(JSON.stringify(initial_message));
                 };
                 connection.onmessage = function(e) {
-                    alert('Received: ' + e.data);
+                    onReceive(e);
                 };
                 connection.onclose = function() {
-                    //alert("closing");
                     connection = null;
                 };
-            });
+            }
+            
+            function disconnect() {
+                connection.close();
+                connection = null;
+            }
+
+            function onReceive(msg) {
+                var obj = jQuery.parseJSON(msg.data);
+                if (obj.game_not_found != null) {
+                    $('#status').html("Game \"" + obj.game_not_found.game_id + "\" not found");
+                }
+                else if (obj.game_already_exists != null) {
+                    $('#status').html("Game \"" + obj.game_already_exists.game_id + "\" already exists");
+                }
+                else if (obj.game_state != null) {
+                    playerlist = "";
+                    for (var i = 0; i < obj.game_state.players.length; i++) {
+                        playerlist = playerlist + ", " + obj.game_state.players[i].nick;
+                    }
+                    $('#status').html("Players: " + playerlist);
+                }
+                else {
+                    $('#status').html("Unknown Message recieved");
+                }
+            }
         });
     </script>
 </head>
@@ -44,6 +83,9 @@
   <label for="nick">Nickname:</label>
   <input id="nick" type="text" /><br>
   <input id="join" type="button" value="Join" />
+  <input id="create" type="button" value="Create Game" />
 </form>
+<div id="status">
+</div>
 </body>
 </html>