Stephen Seo
b2ea79a7f7
When the front-end polls the back-end for the game-state, the back-end includes a "date_updated" String in the JSON. If the String is the same as in the front-end, then no updates are needed, but if they are not the same, then the front-end will update the board. Because the front-end polls the back-end's board state approximately every second, this should make the front-end more efficient.
353 lines
12 KiB
Rust
353 lines
12 KiB
Rust
//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 <https://www.gnu.org/licenses/>.
|
|
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<DBHandlerRequest>,
|
|
_shutdown_tx: SyncSender<()>, // maybe used here, not sure if it will be
|
|
) -> Result<String, String> {
|
|
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<DBHandlerRequest>) -> Result<String, String> {
|
|
let (player_tx, player_rx) = sync_channel::<GetIDSenderType>(1);
|
|
let mut phrase: Option<String> = 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<DBHandlerRequest>) -> Result<String, String> {
|
|
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::<CheckPairingType>(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<DBHandlerRequest>) -> Result<String, String> {
|
|
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<DBHandlerRequest>) -> Result<String, String> {
|
|
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<DBHandlerRequest>) -> Result<String, String> {
|
|
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<DBHandlerRequest>) -> Result<String, String> {
|
|
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<EmoteEnum, ()> = 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())
|
|
}
|
|
}
|