Live location tracking and playback for the game "manhunt"
at main 529 lines 15 kB view raw
1use std::{ 2 collections::{HashMap, HashSet}, 3 net::SocketAddr, 4}; 5 6use axum::{Error as AxumError, extract::ws::Message, http::StatusCode}; 7use matchbox_protocol::PeerId; 8use matchbox_signaling::{ 9 SignalingError, SignalingState, 10 common_logic::{self, StateObj}, 11}; 12use rand::{rngs::ThreadRng, seq::IndexedRandom}; 13use tokio::sync::mpsc::UnboundedSender; 14use tokio_util::sync::CancellationToken; 15 16pub type RoomId = String; 17pub type Sender = UnboundedSender<Result<Message, AxumError>>; 18 19#[derive(Debug, Clone)] 20struct Match { 21 pub open_lobby: bool, 22 cancel: CancellationToken, 23 pub players: HashSet<PeerId>, 24} 25 26#[derive(Debug, Clone)] 27struct Peer { 28 pub room: RoomId, 29 sender: Sender, 30} 31 32impl Match { 33 pub fn new() -> Self { 34 Self { 35 open_lobby: true, 36 cancel: CancellationToken::new(), 37 players: HashSet::with_capacity(10), 38 } 39 } 40} 41 42#[derive(Default, Debug, Clone)] 43pub struct ServerState { 44 waiting_clients: StateObj<HashMap<SocketAddr, (RoomId, bool)>>, 45 queued_clients: StateObj<HashMap<PeerId, (RoomId, bool)>>, 46 matches: StateObj<HashMap<RoomId, Match>>, 47 clients: StateObj<HashMap<PeerId, Peer>>, 48} 49 50impl SignalingState for ServerState {} 51 52#[derive(Debug, Clone, Copy, PartialEq, Eq)] 53pub enum RoomError { 54 /// Room already exists 55 Exists, 56 /// Room was not found 57 NotFound, 58} 59 60impl From<RoomError> for StatusCode { 61 fn from(val: RoomError) -> Self { 62 match val { 63 RoomError::Exists => StatusCode::CONFLICT, 64 RoomError::NotFound => StatusCode::NOT_FOUND, 65 } 66 } 67} 68 69const ROOM_CODE_CHAR_POOL: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"; 70const ROOM_CODE_LEN: usize = 6; 71const MAX_ROOM_TRIES: usize = 25; 72 73#[derive(Debug, Default, Clone, Copy)] 74pub struct NoRoomsError; 75 76impl ServerState { 77 fn random_room_code(rng: &mut ThreadRng) -> RoomId { 78 ROOM_CODE_CHAR_POOL 79 .sample(rng, ROOM_CODE_LEN) 80 .copied() 81 .map(char::from) 82 .collect() 83 } 84 85 fn check_room_taken(&self, code: &RoomId) -> bool { 86 self.matches.lock().unwrap().contains_key(code) 87 } 88 89 pub fn generate_room_code(&self) -> Result<RoomId, NoRoomsError> { 90 let mut rng = rand::rng(); 91 for _ in 0..MAX_ROOM_TRIES { 92 let code = Self::random_room_code(&mut rng); 93 94 if !self.check_room_taken(&code) { 95 return Ok(code); 96 } 97 } 98 Err(NoRoomsError) 99 } 100 101 fn add_client(&mut self, origin: SocketAddr, code: RoomId, host: bool) { 102 self.waiting_clients 103 .lock() 104 .unwrap() 105 .insert(origin, (code.clone(), host)); 106 } 107 108 pub fn room_is_open(&self, room_id: &str) -> bool { 109 self.matches 110 .lock() 111 .unwrap() 112 .get(room_id) 113 .is_some_and(|m| m.open_lobby) 114 } 115 116 /// Mark a match as started, disallowing others from joining 117 pub fn mark_started(&mut self, room: &RoomId) { 118 if let Some(mat) = self.matches.lock().unwrap().get_mut(room) { 119 mat.open_lobby = false; 120 } 121 } 122 123 /// Create a new room with the given code, should be called when someone wants to host a game. 124 /// Returns false if a room with that code already exists. 125 fn create_room(&mut self, origin: SocketAddr, code: RoomId) -> bool { 126 let mut matches = self.matches.lock().unwrap(); 127 if matches.contains_key(&code) { 128 false 129 } else { 130 matches.insert(code.clone(), Match::new()); 131 drop(matches); 132 self.add_client(origin, code, true); 133 true 134 } 135 } 136 137 /// Try to join a room by a code, returns `true` if successful 138 fn try_join_room(&mut self, origin: SocketAddr, code: RoomId) -> bool { 139 if self.room_is_open(&code) { 140 self.waiting_clients 141 .lock() 142 .unwrap() 143 .insert(origin, (code, false)); 144 true 145 } else { 146 false 147 } 148 } 149 150 /// Try to create / join a room 151 pub fn handle_room( 152 &mut self, 153 create: bool, 154 origin: SocketAddr, 155 code: RoomId, 156 ) -> Result<(), RoomError> { 157 match create { 158 true => match self.create_room(origin, code) { 159 true => Ok(()), 160 false => Err(RoomError::Exists), 161 }, 162 false => match self.try_join_room(origin, code) { 163 true => Ok(()), 164 false => Err(RoomError::NotFound), 165 }, 166 } 167 } 168 169 /// Assign a peer an id 170 pub fn assign_peer_id(&mut self, origin: SocketAddr, peer_id: PeerId) { 171 let target_room = self 172 .waiting_clients 173 .lock() 174 .unwrap() 175 .remove(&origin) 176 .expect("origin not waiting?"); 177 178 self.queued_clients 179 .lock() 180 .unwrap() 181 .insert(peer_id, target_room); 182 } 183 184 /// Add a peer to a room, returns other peers in the match currently 185 pub fn add_peer( 186 &mut self, 187 peer_id: PeerId, 188 sender: Sender, 189 ) -> (bool, CancellationToken, Vec<PeerId>) { 190 let (target_room, host) = self 191 .queued_clients 192 .lock() 193 .unwrap() 194 .remove(&peer_id) 195 .expect("peer not waiting?"); 196 let mut matches = self.matches.lock().unwrap(); 197 let mat = matches.get_mut(&target_room).expect("Room not found?"); 198 let peers = mat.players.iter().copied().collect::<Vec<_>>(); 199 mat.players.insert(peer_id); 200 let cancel = mat.cancel.clone(); 201 drop(matches); 202 let peer = Peer { 203 room: target_room, 204 sender, 205 }; 206 self.clients.lock().unwrap().insert(peer_id, peer); 207 (host, cancel, peers) 208 } 209 210 /// Disconnect a peer from a room. Automatically deletes the room if no peers remain. Returns 211 /// the removed peer and the set of other peers in the room that need to be notified 212 pub fn remove_peer(&mut self, peer_id: PeerId, host: bool) -> Option<Vec<PeerId>> { 213 let removed_peer = self.clients.lock().unwrap().remove(&peer_id)?; 214 215 let mut matches = self.matches.lock().unwrap(); 216 217 let other_peers = matches 218 .get_mut(&removed_peer.room) 219 .map(|m| { 220 m.players.remove(&peer_id); 221 m.players.iter().copied().collect::<Vec<_>>() 222 }) 223 .unwrap_or_default(); 224 225 if host { 226 if let Some(mat) = matches.get_mut(&removed_peer.room).filter(|m| m.open_lobby) { 227 // If we're host, disconnect everyone else 228 mat.open_lobby = false; 229 mat.cancel.cancel(); 230 } 231 } 232 233 if other_peers.is_empty() { 234 matches.remove(&removed_peer.room); 235 } 236 237 Some(other_peers) 238 } 239 240 pub fn try_send(&self, peer: PeerId, msg: Message) -> Result<(), SignalingError> { 241 self.clients 242 .lock() 243 .unwrap() 244 .get(&peer) 245 .ok_or(SignalingError::UnknownPeer) 246 .and_then(|peer| common_logic::try_send(&peer.sender, msg)) 247 } 248} 249 250#[cfg(test)] 251mod tests { 252 use std::net::{IpAddr, Ipv4Addr}; 253 254 use uuid::Uuid; 255 256 use super::*; 257 258 const fn origin(p: u16) -> SocketAddr { 259 SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), p) 260 } 261 262 const fn peer(p: u16) -> PeerId { 263 PeerId(Uuid::from_u128(p as u128)) 264 } 265 266 fn dummy_sender() -> Sender { 267 let (s, _) = tokio::sync::mpsc::unbounded_channel(); 268 s 269 } 270 271 fn handle_assign_add(state: &mut ServerState, create: bool, code: &str, p: u16) { 272 state 273 .handle_room(create, origin(p), code.to_string()) 274 .expect("Failed to handle room"); 275 state.assign_peer_id(origin(p), peer(p)); 276 state.add_peer(peer(p), dummy_sender()); 277 } 278 279 fn quick_create(state: &mut ServerState, code: &str, p: u16) { 280 handle_assign_add(state, true, code, p); 281 } 282 283 fn quick_join(state: &mut ServerState, code: &str, p: u16) { 284 handle_assign_add(state, false, code, p); 285 } 286 287 #[test] 288 fn test_add_waiting_host() { 289 let mut state = ServerState::default(); 290 291 let code = "aaa"; 292 293 state 294 .handle_room(true, origin(1), code.to_string()) 295 .expect("Could not create room"); 296 assert_eq!( 297 *state.waiting_clients.lock().unwrap(), 298 HashMap::from_iter([(origin(1), (code.to_string(), true))]) 299 ); 300 assert!(state.room_is_open(code)) 301 } 302 303 #[test] 304 fn test_add_waiting_player() { 305 let mut state = ServerState::default(); 306 307 let code = "aaaa"; 308 309 quick_create(&mut state, code, 1); 310 311 state 312 .handle_room(false, origin(2), code.to_string()) 313 .expect("Failed to join room"); 314 assert_eq!( 315 *state.waiting_clients.lock().unwrap(), 316 HashMap::from_iter([(origin(2), (code.to_string(), false))]) 317 ); 318 } 319 320 #[test] 321 fn test_assign_id() { 322 let mut state = ServerState::default(); 323 324 let code = "aaa"; 325 326 state 327 .handle_room(true, origin(1), code.to_string()) 328 .expect("Could not create room"); 329 330 state.assign_peer_id(origin(1), peer(1)); 331 332 assert!(state.waiting_clients.lock().unwrap().is_empty()); 333 assert_eq!( 334 *state.queued_clients.lock().unwrap(), 335 HashMap::from_iter([(peer(1), (code.to_string(), true))]), 336 ) 337 } 338 339 #[test] 340 fn test_add_peer() { 341 let mut state = ServerState::default(); 342 343 let code = "aaa"; 344 345 state 346 .handle_room(true, origin(1), code.to_string()) 347 .expect("Could not create room"); 348 349 state.assign_peer_id(origin(1), peer(1)); 350 351 let (_, _, others) = state.add_peer(peer(1), dummy_sender()); 352 353 assert!(state.waiting_clients.lock().unwrap().is_empty()); 354 assert!(state.queued_clients.lock().unwrap().is_empty()); 355 assert!(others.is_empty()); 356 assert!( 357 state 358 .clients 359 .lock() 360 .unwrap() 361 .get(&peer(1)) 362 .is_some_and(|p| p.room == code) 363 ); 364 assert!( 365 state 366 .matches 367 .lock() 368 .unwrap() 369 .get(&code.to_string()) 370 .is_some_and(|m| m.players.contains(&peer(1))) 371 ); 372 } 373 374 #[test] 375 fn test_join_add_peer() { 376 let mut state = ServerState::default(); 377 378 let code = "abcd"; 379 380 quick_create(&mut state, code, 1); 381 382 state 383 .handle_room(false, origin(2), code.to_string()) 384 .expect("Failed to join"); 385 state.assign_peer_id(origin(2), peer(2)); 386 387 let (_, _, others) = state.add_peer(peer(2), dummy_sender()); 388 389 assert_eq!(others, vec![peer(1)]); 390 assert!( 391 state 392 .clients 393 .lock() 394 .unwrap() 395 .get(&peer(2)) 396 .is_some_and(|p| p.room == code) 397 ); 398 assert!( 399 state 400 .matches 401 .lock() 402 .unwrap() 403 .get(&code.to_string()) 404 .is_some_and(|m| m.players.contains(&peer(1)) && m.players.contains(&peer(2))) 405 ); 406 } 407 408 #[test] 409 fn test_player_leave() { 410 let mut state = ServerState::default(); 411 412 let code = "asdfasdfasdfasdf"; 413 414 quick_create(&mut state, code, 1); 415 quick_join(&mut state, code, 2); 416 417 let others = state.remove_peer(peer(2), false); 418 419 assert_eq!(others, Some(vec![peer(1)])); 420 assert!( 421 state 422 .matches 423 .lock() 424 .unwrap() 425 .get(&code.to_string()) 426 .is_some_and(|m| m.players.contains(&peer(1)) && !m.players.contains(&peer(2))) 427 ); 428 assert!(!state.clients.lock().unwrap().contains_key(&peer(2))); 429 } 430 431 #[test] 432 fn test_player_leave_only_one() { 433 let mut state = ServerState::default(); 434 435 let code = "asdfasdfasdfasdf"; 436 437 quick_create(&mut state, code, 1); 438 439 let others = state.remove_peer(peer(1), true); 440 441 assert!(others.is_some_and(|v| v.is_empty())); 442 assert!(state.matches.lock().unwrap().is_empty()); 443 assert!(state.clients.lock().unwrap().is_empty()); 444 } 445 446 #[test] 447 fn test_host_leave_with_players() { 448 let mut state = ServerState::default(); 449 450 let code = "asdfasdfasdfasdf"; 451 452 quick_create(&mut state, code, 1); 453 quick_join(&mut state, code, 2); 454 455 let others = state.remove_peer(peer(1), true); 456 457 assert_eq!(others, Some(vec![peer(2)])); 458 let matches = state.matches.lock().unwrap(); 459 let mat = &matches[&code.to_string()]; 460 assert!(mat.cancel.is_cancelled()); 461 assert!(!mat.open_lobby); 462 } 463 464 #[test] 465 fn test_host_leave_with_players_but_started() { 466 let mut state = ServerState::default(); 467 468 let code = "asdfasdfasdfasdf"; 469 470 quick_create(&mut state, code, 1); 471 quick_join(&mut state, code, 2); 472 473 state.mark_started(&code.to_string()); 474 475 let others = state.remove_peer(peer(1), true); 476 477 assert_eq!(others, Some(vec![peer(2)])); 478 let matches = state.matches.lock().unwrap(); 479 let mat = &matches[&code.to_string()]; 480 assert!(!mat.cancel.is_cancelled()); 481 assert!(!mat.open_lobby); 482 } 483 484 #[test] 485 fn test_join_no_match() { 486 let mut state = ServerState::default(); 487 488 let code = "asdfasdf"; 489 490 let res = state.handle_room(false, origin(1), code.to_string()); 491 assert_eq!(res, Err(RoomError::NotFound)); 492 } 493 494 #[test] 495 fn test_create_exists() { 496 let mut state = ServerState::default(); 497 498 let code = "cdf"; 499 500 quick_create(&mut state, code, 1); 501 502 let res = state.handle_room(true, origin(2), code.to_string()); 503 assert_eq!(res, Err(RoomError::Exists)); 504 } 505 506 #[test] 507 fn test_join_started() { 508 let mut state = ServerState::default(); 509 510 let code = "qwerty"; 511 512 quick_create(&mut state, code, 1); 513 quick_join(&mut state, code, 2); 514 515 state.mark_started(&code.to_string()); 516 517 assert!( 518 state 519 .matches 520 .lock() 521 .unwrap() 522 .get(&code.to_string()) 523 .is_some_and(|m| !m.open_lobby) 524 ); 525 526 let res = state.handle_room(false, origin(3), code.to_string()); 527 assert_eq!(res, Err(RoomError::NotFound)); 528 } 529}