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:
parent
234baefb9e
commit
473e76a1bc
2 changed files with 124 additions and 16 deletions
|
@ -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(
|
||||
|
|
|
@ -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> {
|
||||
|
|
Loading…
Reference in a new issue