Skip to content

Commit

Permalink
Implement graceful disconnect
Browse files Browse the repository at this point in the history
  • Loading branch information
TrueDoctor committed May 22, 2024
1 parent e7e252b commit 3f7faf1
Show file tree
Hide file tree
Showing 4 changed files with 102 additions and 17 deletions.
42 changes: 36 additions & 6 deletions backend/src/game.rs
Original file line number Diff line number Diff line change
@@ -1,21 +1,51 @@
use std::net::SocketAddr;

pub(crate) type Position = (u32, u32);

#[derive(Debug, Default, Clone)]
pub(crate) struct Board;
pub(crate) struct Board {
tiles: Vec<Tile>,
pub(crate) width: u16,
pub(crate) height: u16,
}

impl Board {
pub(crate) fn place(&mut self, x: u32, y: u32, addr: SocketAddr) {
todo!()
pub(crate) fn new(width: u16, height: u16) -> Self {
Board {
tiles: vec![Tile::Empty; usize::from(width) * usize::from(height)],
width,
height,
}
}

pub(crate) fn place(&mut self, x: u16, y: u16, id: u8) {
let x = usize::from(x.min(self.width));
let y = usize::from(y.min(self.height));
self.tiles[x + usize::from(self.width) * y] = Tile::Player(id);
}

pub(crate) fn serialize(&self) -> String {
format!("BOARD")
self.tiles.iter().map(Tile::to_char).collect()
}
}

pub enum GoError {
OutOfBounds,
Suicide,
}

#[derive(Debug, Clone, Copy, Default)]
enum Tile {
#[default]
Empty,
Wall,
Player(u8),
}

impl Tile {
fn to_char(&self) -> char {
match self {
Tile::Empty => '.',
Tile::Wall => '/',
Tile::Player(c) => *c as char,
}
}
}
54 changes: 46 additions & 8 deletions backend/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ mod game;
mod network;

use std::io::{ErrorKind, Write};
use std::net::SocketAddr;
use std::time::Duration;
use std::{net::TcpListener, str::FromStr};

use network::{Connection, UserAuth};
Expand All @@ -14,6 +16,8 @@ struct GameState {
users: Vec<Connection>,
user_auth: UserAuth,
board: Board,
chars: Vec<Option<SocketAddr>>,
disconnected: Vec<SocketAddr>,
}

impl GameState {
Expand All @@ -30,24 +34,52 @@ impl GameState {
}
Ok(Command::Put(pos)) => user.next_stone = Some(pos),
Err(Error::WouldBlock) => break,
Err(Error::IO(e)) if e.kind() == ErrorKind::WouldBlock => break,
Err(Error::IO(e)) if e.kind() == ErrorKind::ConnectionAborted => {
// TODO: remove user
Err(Error::ConnectionLost) => {
self.disconnected.push(user.addr);
eprintln!("Lost connection to {}", user.addr);
break;
}
Err(error) => eprintln!("error while reading user input: {error}"),
}
}
}
}

pub(crate) fn alloc_char(&mut self, addr: SocketAddr) -> Option<u8> {
let pos = self.chars.iter().position(Option::is_none)?;
self.chars[pos] = Some(addr);
Some(pos as u8 + b'A')
}

fn remove_user(&mut self, addr: SocketAddr) {
eprintln!("Removing user {}", addr);
if let Some(pos) = self.users.iter().position(|u| u.addr == addr) {
self.users.swap_remove(pos);
}
if let Some(value) = self.chars.iter_mut().find(|x| **x == Some(addr)) {
std::mem::take(value);
}
}

fn remove_disconnected_users(&mut self) {
for addr in std::mem::take(&mut self.disconnected) {
self.remove_user(addr);
}
}

fn broadcast_gamestate(&mut self) {
let state = self.board.serialize();
for user in self.users.iter_mut() {
match user.stream.write_all(state.as_bytes()) {
Err(e) if e.kind() == ErrorKind::ConnectionAborted => {
// TODO: remove user
eprintln!("Lost connection to {}", user.addr);
match writeln!(
user.stream,
"BOARD {} {} {} {}",
user.char, self.board.width, self.board.height, state
) {
Err(e) if e.kind() == ErrorKind::ConnectionAborted => (),
Err(e) if e.kind() == ErrorKind::BrokenPipe => (),
Err(e) if matches!(e.kind(), ErrorKind::ConnectionAborted | ErrorKind::BrokenPipe) => {
dbg!("foo");
self.disconnected.push(user.addr);
}
Err(e) => {
eprintln!("Error while sending {e}");
Expand All @@ -61,13 +93,19 @@ impl GameState {
fn main() -> std::io::Result<()> {
let listener = TcpListener::bind("127.0.0.1:1312")?;
listener.set_nonblocking(true)?;
let mut game = GameState::default();
let mut game = GameState {
board: Board::new(10, 10),
chars: vec![None; 'z' as usize - 'A' as usize],
..Default::default()
};

loop {
if let Err(e) = network::accept_new_connections(&listener, &mut game) {
eprintln!("Error while accepting a new connection: {e}");
}
game.process_user_input();
game.remove_disconnected_users();
game.broadcast_gamestate();
std::thread::sleep(Duration::from_millis(500));
}
}
19 changes: 17 additions & 2 deletions backend/src/network.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,20 @@ pub enum Error {
InvalidArgument,
UnknownCommand,
InvalidCredentials,
ConnectionLost,
WouldBlock,
GameFull,
IO(std::io::Error),
Utf8(std::str::Utf8Error),
}

impl From<std::io::Error> for Error {
fn from(value: std::io::Error) -> Self {
Error::IO(value)
match value.kind() {
ErrorKind::WouldBlock => Error::WouldBlock,
ErrorKind::BrokenPipe | ErrorKind::ConnectionAborted => Error::ConnectionLost,
_ => Error::IO(value),
}
}
}
impl From<std::str::Utf8Error> for Error {
Expand All @@ -36,6 +42,7 @@ pub(crate) struct Connection {
pub(crate) addr: SocketAddr,
pub(crate) username: Option<String>,
color: Color,
pub(crate) char: u8,
pub(crate) stream: TcpStream,
pub(crate) next_stone: Option<Position>,
}
Expand All @@ -46,6 +53,7 @@ impl Display for Error {
}
}

#[derive(Debug, Clone)]
pub(crate) enum Command {
Login(String, String),
Put(Position),
Expand Down Expand Up @@ -75,6 +83,9 @@ pub(crate) fn parse_line(
) -> Result<Command, Error> {
let mut buf = [0; 1024];
let bytes = stream.peek(&mut buf)?;
if bytes == 0 {
return Err(Error::ConnectionLost);
}
let pos = buf[0..bytes]
.iter()
.position(|a| a == &b'\n')
Expand All @@ -87,18 +98,22 @@ pub(crate) fn parse_line(
panic!("{:?}", &buf[..bytes]);
}
}
pub(crate) fn accept_new_connections(listener: &TcpListener, game: &mut GameState) -> std::io::Result<()> {
pub(crate) fn accept_new_connections(listener: &TcpListener, game: &mut GameState) -> Result<(), Error> {
fn random_color() -> Color {
std::collections::hash_map::DefaultHasher::new().finish() as Color
}
loop {
match listener.accept() {
Ok((stream, addr)) => {
stream.set_nonblocking(true)?;
let Some(char) = game.alloc_char(addr) else {
return Err(Error::GameFull);
};
let con = Connection {
addr,
username: None,
color: random_color(),
char,
stream,
next_stone: None,
};
Expand Down
4 changes: 3 additions & 1 deletion client/src/game.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ impl GameState {
}
}

#[derive(Debug, Default, Clone)]
pub struct Map {
width: u16,
height: u16,
Expand Down Expand Up @@ -60,8 +61,9 @@ impl Map {
}
}

#[derive(Clone, Copy)]
#[derive(Clone, Copy, Default, Debug)]
pub enum Tile {
#[default]
Empty,
Wall,
Player(u8),
Expand Down

0 comments on commit 3f7faf1

Please sign in to comment.