//Four Line Dropper Backend - A server enabling networked multiplayer for Four Line Dropper //Copyright (C) 2022 Stephen Seo // //This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. // //This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // //You should have received a copy of the GNU General Public License along with this program. If not, see . use crate::{ constants::BACKEND_PHRASE_MAX_LENGTH, db_handler::{CheckPairingType, DBHandlerRequest, GetIDSenderType}, state::EmoteEnum, }; use std::{ sync::mpsc::{sync_channel, SyncSender}, time::Duration, }; use serde_json::Value; const DB_REQUEST_TIMEOUT: Duration = Duration::from_secs(5); pub fn handle_json( root: Value, tx: SyncSender, _shutdown_tx: SyncSender<()>, // maybe used here, not sure if it will be ) -> Result { if let Some(Value::String(type_str)) = root.get("type") { match type_str.as_str() { "pairing_request" => handle_pairing_request(root, tx), "check_pairing" => handle_check_pairing(root, tx), "place_token" => handle_place_token(root, tx), "disconnect" => handle_disconnect(root, tx), "game_state" => handle_game_state(root, tx), "send_emote" => handle_send_emote(root, tx), _ => Err("{\"type\":\"invalid_type\"}".into()), } } else { Err("{\"type\":\"invalid_json\"}".into()) } } fn handle_pairing_request(root: Value, tx: SyncSender) -> Result { let (player_tx, player_rx) = sync_channel::(1); let mut phrase: Option = None; if let Some(phrase_text) = root.get("phrase") { if let Some(mut phrase_str) = phrase_text.as_str() { if !phrase_str.is_empty() { if phrase_str.len() > BACKEND_PHRASE_MAX_LENGTH { let mut idx = BACKEND_PHRASE_MAX_LENGTH; while idx > 0 && !phrase_str.is_char_boundary(idx) { idx -= 1; } if idx == 0 { phrase_str = ""; } else { phrase_str = phrase_str.split_at(idx).0; } } if !phrase_str.is_empty() { phrase = Some(phrase_str.to_owned()); } } } } if tx .send(DBHandlerRequest::GetID { response_sender: player_tx, phrase, }) .is_err() { return Err("{\"type\":\"pairing_response\", \"status\":\"internal_error\"}".into()); } if let Ok((pid_opt, is_cyan_opt)) = player_rx.recv_timeout(DB_REQUEST_TIMEOUT) { if pid_opt.is_none() { return Ok("{\"type\":\"pairing_response\", \"status\":\"too_many_players\"}".into()); } let pid = pid_opt.unwrap(); if let Some(is_cyan) = is_cyan_opt { Ok(format!( "{{\"type\":\"pairing_response\", \"id\": {}, \"status\": \"paired\", \"color\": \"{}\"}}", pid, if is_cyan { "cyan" } else { "magenta" } )) } else { Ok(format!( "{{\"type\":\"pairing_response\", \"id\": {}, \"status\": \"waiting\"}}", pid )) } } else { Err("{\"type\":\"pairing_response\", \"status\":\"internal_error_timeout\"}".into()) } } fn handle_check_pairing(root: Value, tx: SyncSender) -> Result { let id_option = root.get("id"); if id_option.is_none() { return Err("{\"type\":\"invalid_syntax\"}".into()); } let player_id = id_option .unwrap() .as_u64() .ok_or_else(|| String::from("{\"type\":\"invalid_syntax\"}"))?; let player_id: u32 = player_id .try_into() .map_err(|_| String::from("{\"type\":\"invalid_syntax\"}"))?; let (request_tx, request_rx) = sync_channel::(1); if tx .send(DBHandlerRequest::CheckPairing { id: player_id, response_sender: request_tx, }) .is_err() { return Err("{\"type\":\"pairing_status\", \"status\":\"internal_error\"}".into()); } if let Ok((exists, is_paired, is_cyan)) = request_rx.recv_timeout(DB_REQUEST_TIMEOUT) { if !exists { Err("{\"type\":\"pairing_status\", \"status\":\"unknown_id\"}".into()) } else if is_paired { Ok(format!( "{{\"type\":\"pairing_status\", \"status\":\"paired\", \"color\":\"{}\"}}", if is_cyan { "cyan" } else { "magenta" } )) } else { Ok("{\"type\":\"pairing_status\", \"status\":\"waiting\"}".into()) } } else { Err("{\"type\":\"pairing_status\", \"status\":\"internal_error_timeout\"}".into()) } } fn handle_place_token(root: Value, tx: SyncSender) -> Result { let id_option = root.get("id"); if id_option.is_none() { return Err("{\"type\":\"invalid_syntax\"}".into()); } let player_id = id_option .unwrap() .as_u64() .ok_or_else(|| String::from("{\"type\":\"invalid_syntax\"}"))?; let player_id: u32 = player_id .try_into() .map_err(|_| String::from("{\"type\":\"invalid_syntax\"}"))?; let position_option = root.get("position"); if position_option.is_none() { return Err("{\"type\":\"invalid_syntax\"}".into()); } let position = position_option .unwrap() .as_u64() .ok_or_else(|| String::from("{\"type\":\"invalid_syntax\"}"))?; let position: usize = position .try_into() .map_err(|_| String::from("{\"type\":\"invalid_syntax\"}"))?; let (resp_tx, resp_rx) = sync_channel(1); if tx .send(DBHandlerRequest::PlaceToken { id: player_id, pos: position, response_sender: resp_tx, }) .is_err() { return Err(String::from( "{\"type\":\"place_token\", \"status\":\"internal_error\"}", )); } let place_result = resp_rx.recv_timeout(DB_REQUEST_TIMEOUT); if let Ok(Ok((place_status, board_opt))) = place_result { if let Some(board_string) = board_opt { Ok(format!( "{{\"type\":\"place_token\", \"status\":\"{}\", \"board\":\"{}\"}}", place_status, board_string )) } else { Ok(format!( "{{\"type\":\"place_token\", \"status\":\"{}\"}}", place_status )) } } else if let Ok(Err(place_error)) = place_result { Err(format!( "{{\"type\":\"place_token\", \"status\":\"{}\"}}", place_error )) } else { Err(String::from( "{\"type\":\"place_token\", \"status\":\"internal_error\"}", )) } } fn handle_disconnect(root: Value, tx: SyncSender) -> Result { let id_option = root.get("id"); if id_option.is_none() { return Err("{\"type\":\"invalid_syntax\"}".into()); } let player_id = id_option .unwrap() .as_u64() .ok_or_else(|| String::from("{\"type\":\"invalid_syntax\"}"))?; let player_id: u32 = player_id .try_into() .map_err(|_| String::from("{\"type\":\"invalid_syntax\"}"))?; let (resp_tx, resp_rx) = sync_channel(1); if tx .send(DBHandlerRequest::DisconnectID { id: player_id, response_sender: resp_tx, }) .is_err() { return Err(String::from( "{\"type\":\"disconnect\", \"status\":\"internal_error\"}", )); } if let Ok(was_removed) = resp_rx.recv_timeout(DB_REQUEST_TIMEOUT) { if was_removed { Ok(String::from("{\"type\":\"disconnect\", \"status\":\"ok\"}")) } else { Ok(String::from( "{\"type\":\"disconnect\", \"status\":\"unknown_id\"}", )) } } else { Err(String::from( "{\"type\":\"disconnect\", \"status\":\"internal_error\"}", )) } } fn handle_game_state(root: Value, tx: SyncSender) -> Result { let id_option = root.get("id"); if id_option.is_none() { return Err("{\"type\":\"invalid_syntax\"}".into()); } let player_id = id_option .unwrap() .as_u64() .ok_or_else(|| String::from("{\"type\":\"invalid_syntax\"}"))?; let player_id: u32 = player_id .try_into() .map_err(|_| String::from("{\"type\":\"invalid_syntax\"}"))?; let (resp_tx, resp_rx) = sync_channel(1); if tx .send(DBHandlerRequest::GetGameState { id: player_id, response_sender: resp_tx, }) .is_err() { return Err("{\"type\":\"game_state\", \"status\":\"internal_error\"}".into()); } if let Ok((db_game_state, board_string_opt, updated_time_opt, received_emote_opt)) = resp_rx.recv_timeout(DB_REQUEST_TIMEOUT) { if let Some(board_string) = board_string_opt { let updated_time = if let Some(time_string) = updated_time_opt { time_string } else { return Err("{\"type\":\"game_state\", \"status\":\"internal_error\"}".into()); }; if let Some(emote) = received_emote_opt { Ok(format!( "{{\"type\":\"game_state\", \"status\":\"{}\", \"board\":\"{}\", \"peer_emote\": \"{}\", \"updated_time\": \"{}\"}}", db_game_state, board_string, emote, updated_time )) } else { Ok(format!( "{{\"type\":\"game_state\", \"status\":\"{}\", \"board\":\"{}\", \"updated_time\": \"{}\"}}", db_game_state, board_string, updated_time )) } } else { Ok(format!( "{{\"type\":\"game_state\", \"status\":\"{}\"}}", db_game_state )) } } else { Err("{\"type\":\"game_state\", \"status\":\"internal_error_timeout\"}".into()) } } fn handle_send_emote(root: Value, tx: SyncSender) -> Result { let id_option = root.get("id"); if id_option.is_none() { return Err("{\"type\":\"invalid_syntax\"}".into()); } let player_id = id_option .unwrap() .as_u64() .ok_or_else(|| String::from("{\"type\":\"invalid_syntax\"}"))?; let player_id: u32 = player_id .try_into() .map_err(|_| String::from("{\"type\":\"invalid_syntax\"}"))?; let emote_type_option = root.get("emote"); if emote_type_option.is_none() { return Err("{\"type\":\"invalid_syntax\"}".into()); } let emote_type_option = emote_type_option.unwrap().as_str(); if emote_type_option.is_none() { return Err("{\"type\":\"invalid_syntax\"}".into()); } let emote_type = emote_type_option.unwrap(); let emote_enum: Result = emote_type.try_into(); if emote_enum.is_err() { return Err("{\"type\":\"invalid_syntax\"}".into()); } let emote_enum = emote_enum.unwrap(); let (resp_tx, resp_rx) = sync_channel(1); if tx .send(DBHandlerRequest::SendEmote { id: player_id, emote_type: emote_enum, response_sender: resp_tx, }) .is_err() { return Err("{\"type\":\"send_emote\", \"status\":\"internal_error\"}".into()); } if let Ok(db_response) = resp_rx.recv_timeout(DB_REQUEST_TIMEOUT) { if db_response.is_ok() { Ok("{\"type\":\"send_emote\", \"status\":\"ok\"}".into()) } else { Err("{\"type\":\"send_emote\", \"status\":\"internal_error\"}".into()) } } else { Err("{\"type\":\"send_emote\", \"status\":\"internal_error\"}".into()) } }