]> git.seodisparate.com - EN605.607.81.SP22_ASDM_Project/commitdiff
Impl disconnect protocol (and related changes)
authorStephen Seo <seo.disparate@gmail.com>
Thu, 31 Mar 2022 08:38:03 +0000 (17:38 +0900)
committerStephen Seo <seo.disparate@gmail.com>
Thu, 31 Mar 2022 08:38:03 +0000 (17:38 +0900)
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.

back_end/src/db_handler.rs
back_end/src/json_handlers.rs

index 9d2c9aba0a4ab727feb2ee883624b2cd1ae51295..f77a97793a074b5ab97f5ab31bde3c105fd09782 100644 (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(
index b69168b20873c5c8a34ae5ed9382f90c2624181c..d50e51abee7c4355c8273c9495de19a389f46f5d 100644 (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> {