use std::sync::Arc; use std::time::{Duration, Instant}; use actix::prelude::*; use actix::{Actor, Handler, StreamHandler}; use actix_web_actors::ws; use actix_web_actors::ws::{CloseCode, CloseReason, Message, ProtocolError, WebsocketContext}; use uuid::Uuid; use crate::bot_player::BotPlayer; use crate::data::{BoatsLayout, Coordinates, CurrentGameStatus, FireResult, GameRules}; use crate::dispatcher_actor::{AcceptInvite, CreateInvite, DispatcherActor, PlayRandom}; use crate::game::{AddPlayer, Game}; use crate::human_player::HumanPlayer; /// How often heartbeat pings are sent const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(10); /// How long before lack of client response causes a timeout const CLIENT_TIMEOUT: Duration = Duration::from_secs(120); #[derive(Debug)] pub enum StartMode { Bot(GameRules), CreateInvite(GameRules), AcceptInvite { code: String }, PlayRandom, } #[derive(serde::Deserialize, serde::Serialize, Debug)] #[serde(tag = "type")] pub enum ClientMessage { StopGame, BoatsLayout { layout: BoatsLayout }, Fire { location: Coordinates }, RequestRematch, AcceptRematch, RejectRematch, } #[derive(Message)] #[rtype(result = "()")] #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] #[serde(tag = "type")] pub enum ServerMessage { SetInviteCode { code: String, }, InvalidInviteCode, WaitingForAnotherPlayer, OpponentConnected, SetOpponentName { name: String, }, QueryBoatsLayout { rules: GameRules, }, RejectedBoatsLayout { errors: Vec, }, WaitingForOtherPlayerConfiguration, OpponentReady, GameStarting, OpponentMustFire { status: CurrentGameStatus, }, RequestFire { status: CurrentGameStatus, }, FireResult { pos: Coordinates, result: FireResult, }, OpponentFireResult { pos: Coordinates, result: FireResult, }, LostGame { status: CurrentGameStatus, }, WonGame { status: CurrentGameStatus, }, OpponentRequestedRematch, OpponentAcceptedRematch, OpponentRejectedRematch, OpponentLeftGame, OpponentReplacedByBot, } #[derive(Message)] #[rtype(result = "()")] pub struct SetGame(pub Addr); #[derive(Message)] #[rtype(result = "()")] pub struct CloseConnection; pub struct HumanPlayerWS { inner: Option>, pub start_mode: StartMode, hb: Instant, dispatcher: Addr, name: String, } impl HumanPlayerWS { pub fn new(start_mode: StartMode, dispatcher: &Addr, name: String) -> Self { Self { inner: None, start_mode, hb: Instant::now(), dispatcher: dispatcher.clone(), name, } } /// helper method that sends ping to client every second. /// /// also this method checks heartbeats from client fn hb(&self, ctx: &mut ::Context) { ctx.run_interval(HEARTBEAT_INTERVAL, |act, ctx| { // check client heartbeats if Instant::now().duration_since(act.hb) > CLIENT_TIMEOUT { // heartbeat timed out println!("Websocket Client heartbeat failed, disconnecting!"); // stop actor ctx.stop(); // don't try to send a ping return; } ctx.ping(b""); }); } fn send_message(&self, msg: ServerMessage, ctx: &mut ::Context) { ctx.text(serde_json::to_string(&msg).unwrap()); } } impl Actor for HumanPlayerWS { type Context = WebsocketContext; fn started(&mut self, ctx: &mut Self::Context) { self.hb(ctx); self.send_message(ServerMessage::WaitingForAnotherPlayer, ctx); // Start game, according to appropriate start mode match &self.start_mode { StartMode::Bot(rules) => { log::debug!("Start play with a bot"); let game = Game::new(rules.clone()).start(); game.do_send(AddPlayer(Arc::new(BotPlayer::new( rules.bot_type, game.clone(), )))); let player = Arc::new(HumanPlayer { name: self.name.to_string(), game: game.clone(), player: ctx.address(), uuid: Uuid::new_v4(), }); self.inner = Some(player.clone()); game.do_send(AddPlayer(player)); } StartMode::CreateInvite(rules) => { log::info!("Create new play invite"); self.dispatcher .do_send(CreateInvite(rules.clone(), ctx.address())); } StartMode::AcceptInvite { code } => { log::info!("Accept play invite {}", code); self.dispatcher .do_send(AcceptInvite(code.clone(), ctx.address())); } StartMode::PlayRandom => { log::info!("Start random play"); self.dispatcher.do_send(PlayRandom(ctx.address())) } } } fn stopped(&mut self, _ctx: &mut Self::Context) { if let Some(player) = &self.inner { player.handle_client_message(ClientMessage::StopGame); } } } impl StreamHandler> for HumanPlayerWS { fn handle(&mut self, msg: Result, ctx: &mut Self::Context) { match msg { Ok(Message::Ping(msg)) => { self.hb = Instant::now(); ctx.pong(&msg); } Ok(Message::Binary(_bin)) => log::warn!("Got unsupported binary message!"), Ok(Message::Text(msg)) => match serde_json::from_str::(&msg) { Ok(msg) => match &self.inner { None => { log::error!("Client tried to send message without game!"); ctx.text("No game yet!"); } Some(p) => p.handle_client_message(msg), }, Err(e) => log::warn!("Got invalid message from client! {:?}", e), }, Ok(Message::Nop) => log::warn!("Got WS nop"), Ok(Message::Continuation(_)) => { log::warn!("Got unsupported continuation message!"); } Ok(Message::Pong(_)) => { log::info!("Got pong message"); self.hb = Instant::now(); } Ok(Message::Close(reason)) => { log::info!("Client asked to close this socket! reason={:?}", reason); ctx.close(Some(CloseReason::from(CloseCode::Away))); } Err(e) => log::warn!("Websocket protocol error! {:?}", e), } } } impl Handler for HumanPlayerWS { type Result = (); fn handle(&mut self, msg: ServerMessage, ctx: &mut Self::Context) -> Self::Result { ctx.text(serde_json::to_string(&msg).unwrap()); } } impl Handler for HumanPlayerWS { type Result = (); fn handle(&mut self, msg: SetGame, ctx: &mut Self::Context) -> Self::Result { let game = msg.0; let player = Arc::new(HumanPlayer { name: self.name.clone(), game: game.clone(), player: ctx.address(), uuid: Uuid::new_v4(), }); self.inner = Some(player.clone()); game.do_send(AddPlayer(player)); } } impl Handler for HumanPlayerWS { type Result = (); fn handle(&mut self, _msg: CloseConnection, ctx: &mut Self::Context) -> Self::Result { ctx.close(None) } }