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::bots::intermediate_bot::IntermediateBot; use crate::bots::linear_bot::LinearBot; use crate::bots::random_bot::RandomBot; use crate::bots::smart_bot::SmartBot; use crate::data::{BoatsLayout, BotType, Coordinates, CurrentGameStatus, FireResult, GameRules}; 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(Default, Debug)] pub enum StartMode { Bot(GameRules), #[default] RandomHuman, //TODO : create invite } #[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 { WaitingForAnotherPlayer, 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, } pub struct HumanPlayerWS { inner: Option>, pub start_mode: StartMode, hb: Instant, } impl Default for HumanPlayerWS { fn default() -> Self { Self { inner: None, start_mode: Default::default(), hb: Instant::now(), } } } impl HumanPlayerWS { /// 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(); match rules.bot_type { BotType::Random => { game.do_send(AddPlayer(Arc::new(RandomBot::new(game.clone())))); } BotType::Linear => { game.do_send(AddPlayer(Arc::new(LinearBot::new(game.clone())))); } BotType::Intermediate => { game.do_send(AddPlayer(Arc::new(IntermediateBot::new(game.clone())))); } BotType::Smart => { game.do_send(AddPlayer(Arc::new(SmartBot::new(game.clone())))); } }; let player = Arc::new(HumanPlayer { name: "Human".to_string(), game: game.clone(), player: ctx.address(), uuid: Uuid::new_v4(), }); self.inner = Some(player.clone()); game.do_send(AddPlayer(player)); } StartMode::RandomHuman => { unimplemented!(); } } } 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()); } }