Impl disconnect protocol (and related changes)

Players can now make a "disconnect" request, and requests for
"game_state" will respond once that an opponent has disconnected before
removing the game from the DB.
This commit is contained in:
Stephen Seo 2022-03-31 17:38:03 +09:00
parent 234baefb9e
commit 473e76a1bc
2 changed files with 124 additions and 16 deletions

View File

@ -67,6 +67,10 @@ pub enum DBHandlerRequest {
id: u32,
response_sender: SyncSender<BoardStateType>,
},
DisconnectID {
id: u32,
response_sender: SyncSender<bool>,
},
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
@ -181,6 +185,16 @@ impl DBHandler {
// dropped the receiver
response_sender.send(get_board_result.unwrap()).ok();
}
DBHandlerRequest::DisconnectID {
id,
response_sender,
} => {
// don't stop server on send fail, may have timed out and
// dropped the receiver
response_sender
.send(self.disconnect_player(None, id).is_ok())
.ok();
}
} // match db_request
false
@ -215,8 +229,8 @@ impl DBHandler {
date_added TEXT NOT NULL,
board TEXT NOT NULL,
status INTEGER NOT NULL,
FOREIGN KEY(cyan_player) REFERENCES players (id),
FOREIGN KEY(magenta_player) REFERENCES players (id));
FOREIGN KEY(cyan_player) REFERENCES players (id) ON DELETE SET NULL,
FOREIGN KEY(magenta_player) REFERENCES players (id) ON DELETE SET NULL);
",
[],
);
@ -370,9 +384,6 @@ impl DBHandler {
}
}
// clippy lint allow required due to conn.query_row() needing to handle
// returning a tuple in a Result
#[allow(clippy::unnecessary_unwrap)]
fn get_board_state(
&self,
conn: Option<&Connection>,
@ -384,28 +395,56 @@ impl DBHandler {
let conn = conn.unwrap();
// TODO maybe handle "opponent_disconnected" case
let row_result: Result<(String, i64), RusqliteError> =
let row_result: Result<(String, i64, Option<u32>, Option<u32>), RusqliteError> =
conn.query_row(
"SELECT games.board, games.status FROM games JOIN players WHERE players.id = ? AND games.id = players.game_id;",
"SELECT games.board, games.status, games.cyan_player, games.magenta_player FROM games JOIN players WHERE players.id = ? AND games.id = players.game_id;",
[player_id],
|row| {
let board_result = row.get(0);
let status_result = row.get(1);
if board_result.is_ok() && status_result.is_ok() {
Ok((board_result.unwrap(), status_result.unwrap()))
let cyan_player = row.get(2);
let magenta_player = row.get(3);
if board_result.is_ok() && status_result.is_ok() && cyan_player.is_ok() && magenta_player.is_ok() {
if let (Ok(board), Ok(status), Ok(cyan_id), Ok(magenta_id)) = (board_result, status_result, cyan_player, magenta_player) {
Ok((board, status, cyan_id, magenta_id))
} else {
unreachable!("Both row items should be Ok");
}
} else if board_result.is_err() {
board_result
.map(|_| (String::from("this value should never be returned"), 0))
} else {
.map(|_| (String::from("this value should never be returned"), 0, None, None))
} else if status_result.is_err() {
status_result
.map(|_| (String::from("this value should never be returned"), 0))
.map(|_| (String::from("this value should never be returned"), 0, None, None))
} else if cyan_player.is_err() {
cyan_player
.map(|_| (String::from("this value should never be returned"), 0, None, None))
} else {
magenta_player
.map(|_| (String::from("this value should never be returned"), 0, None, None))
}
}
);
if let Ok((board, status)) = row_result {
if let Ok((board, status, cyan_opt, magenta_opt)) = row_result {
if board.len() != (ROWS * COLS) as usize {
// board is invalid size
Ok((DBGameState::InternalError, None))
} else if cyan_opt.is_none() || magenta_opt.is_none() {
// One player disconnected
let player_remove_result = self.disconnect_player(Some(conn), player_id);
if player_remove_result.is_err() {
// Failed to disconnect remaining player
Ok((DBGameState::InternalError, None))
} else {
// Remove the game(s) with disconnected players
if self.clear_empty_games(Some(conn)).is_err() {
Ok((DBGameState::InternalError, None))
} else {
Ok((DBGameState::OpponentDisconnected, Some(board)))
}
}
} else {
// Game in progress, or other state depending on "status"
Ok((DBGameState::from(status), Some(board)))
}
} else if let Err(RusqliteError::QueryReturnedNoRows) = row_result {
@ -424,6 +463,37 @@ impl DBHandler {
Err(String::from("internal_error"))
}
}
fn disconnect_player(&self, conn: Option<&Connection>, player_id: u32) -> Result<(), String> {
if conn.is_none() {
return self
.disconnect_player(Some(&self.get_conn(DBFirstRun::NotFirstRun)?), player_id);
}
let conn = conn.unwrap();
let stmt_result = conn.execute("DELETE FROM players WHERE id = ?;", [player_id]);
if let Ok(1) = stmt_result {
Ok(())
} else {
Err(String::from("id not found"))
}
}
fn clear_empty_games(&self, conn: Option<&Connection>) -> Result<(), String> {
if conn.is_none() {
return self.clear_empty_games(Some(&self.get_conn(DBFirstRun::NotFirstRun)?));
}
let conn = conn.unwrap();
// Only fails if no rows were removed, and that is not an issue
conn.execute(
"DELETE FROM games WHERE cyan_player ISNULL AND magenta_player ISNULL;",
[],
)
.ok();
Ok(())
}
}
pub fn start_db_handler_thread(

View File

@ -19,7 +19,7 @@ pub fn handle_json(
"pairing_request" => handle_pairing_request(tx),
"check_pairing" => handle_check_pairing(root, tx),
"place_token" => handle_place_token(root),
"disconnect" => handle_disconnect(root),
"disconnect" => handle_disconnect(root, tx),
"game_state" => handle_game_state(root, tx),
_ => Err("{\"type\":\"invalid_type\"}".into()),
}
@ -93,8 +93,46 @@ fn handle_place_token(root: Value) -> Result<String, String> {
Err("{\"type\":\"unimplemented\"}".into())
}
fn handle_disconnect(root: Value) -> Result<String, String> {
Err("{\"type\":\"unimplemented\"}".into())
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> {