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,
|
id: u32,
|
||||||
response_sender: SyncSender<BoardStateType>,
|
response_sender: SyncSender<BoardStateType>,
|
||||||
},
|
},
|
||||||
|
DisconnectID {
|
||||||
|
id: u32,
|
||||||
|
response_sender: SyncSender<bool>,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||||
|
@ -181,6 +185,16 @@ impl DBHandler {
|
||||||
// dropped the receiver
|
// dropped the receiver
|
||||||
response_sender.send(get_board_result.unwrap()).ok();
|
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
|
} // match db_request
|
||||||
|
|
||||||
false
|
false
|
||||||
|
@ -215,8 +229,8 @@ impl DBHandler {
|
||||||
date_added TEXT NOT NULL,
|
date_added TEXT NOT NULL,
|
||||||
board TEXT NOT NULL,
|
board TEXT NOT NULL,
|
||||||
status INTEGER NOT NULL,
|
status INTEGER NOT NULL,
|
||||||
FOREIGN KEY(cyan_player) REFERENCES players (id),
|
FOREIGN KEY(cyan_player) REFERENCES players (id) ON DELETE SET NULL,
|
||||||
FOREIGN KEY(magenta_player) REFERENCES players (id));
|
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(
|
fn get_board_state(
|
||||||
&self,
|
&self,
|
||||||
conn: Option<&Connection>,
|
conn: Option<&Connection>,
|
||||||
|
@ -384,28 +395,56 @@ impl DBHandler {
|
||||||
let conn = conn.unwrap();
|
let conn = conn.unwrap();
|
||||||
|
|
||||||
// TODO maybe handle "opponent_disconnected" case
|
// 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(
|
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],
|
[player_id],
|
||||||
|row| {
|
|row| {
|
||||||
let board_result = row.get(0);
|
let board_result = row.get(0);
|
||||||
let status_result = row.get(1);
|
let status_result = row.get(1);
|
||||||
if board_result.is_ok() && status_result.is_ok() {
|
let cyan_player = row.get(2);
|
||||||
Ok((board_result.unwrap(), status_result.unwrap()))
|
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() {
|
} else if board_result.is_err() {
|
||||||
board_result
|
board_result
|
||||||
.map(|_| (String::from("this value should never be returned"), 0))
|
.map(|_| (String::from("this value should never be returned"), 0, None, None))
|
||||||
} else {
|
} else if status_result.is_err() {
|
||||||
status_result
|
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 {
|
if board.len() != (ROWS * COLS) as usize {
|
||||||
|
// board is invalid size
|
||||||
Ok((DBGameState::InternalError, None))
|
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 {
|
} else {
|
||||||
|
// Game in progress, or other state depending on "status"
|
||||||
Ok((DBGameState::from(status), Some(board)))
|
Ok((DBGameState::from(status), Some(board)))
|
||||||
}
|
}
|
||||||
} else if let Err(RusqliteError::QueryReturnedNoRows) = row_result {
|
} else if let Err(RusqliteError::QueryReturnedNoRows) = row_result {
|
||||||
|
@ -424,6 +463,37 @@ impl DBHandler {
|
||||||
Err(String::from("internal_error"))
|
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(
|
pub fn start_db_handler_thread(
|
||||||
|
|
|
@ -19,7 +19,7 @@ pub fn handle_json(
|
||||||
"pairing_request" => handle_pairing_request(tx),
|
"pairing_request" => handle_pairing_request(tx),
|
||||||
"check_pairing" => handle_check_pairing(root, tx),
|
"check_pairing" => handle_check_pairing(root, tx),
|
||||||
"place_token" => handle_place_token(root),
|
"place_token" => handle_place_token(root),
|
||||||
"disconnect" => handle_disconnect(root),
|
"disconnect" => handle_disconnect(root, tx),
|
||||||
"game_state" => handle_game_state(root, tx),
|
"game_state" => handle_game_state(root, tx),
|
||||||
_ => Err("{\"type\":\"invalid_type\"}".into()),
|
_ => Err("{\"type\":\"invalid_type\"}".into()),
|
||||||
}
|
}
|
||||||
|
@ -93,8 +93,46 @@ fn handle_place_token(root: Value) -> Result<String, String> {
|
||||||
Err("{\"type\":\"unimplemented\"}".into())
|
Err("{\"type\":\"unimplemented\"}".into())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_disconnect(root: Value) -> Result<String, String> {
|
fn handle_disconnect(root: Value, tx: SyncSender<DBHandlerRequest>) -> Result<String, String> {
|
||||||
Err("{\"type\":\"unimplemented\"}".into())
|
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> {
|
fn handle_game_state(root: Value, tx: SyncSender<DBHandlerRequest>) -> Result<String, String> {
|
||||||
|
|
Loading…
Reference in a new issue