···3030- [Rust](https://rustup.rs)
3131- [Just](https://just.systems) (`cargo install just`)
3232 - **On Windows**: Some implementation of `sh` (Git for Windows works well)
3333+- Cargo Nextest (only needed for running `just check-rust`, `cargo install cargo-nextest`)
3334- [Tauri's Pre-reqs](https://tauri.app/start/prerequisites/)
3435 - [(Also pre-reqs for mobile dev if you are working on the app part)](https://tauri.app/start/prerequisites/#configure-for-mobile-targets)
3536- Tauri's CLI (`cargo install tauri-cli`)
···59606061### Project Layout
61626262-- [backend/](https://github.com/Bwc9876/manhunt-app/tree/main/backend): App
6363+- [manhunt-logic/](https://github.com/Bwc9876/manhunt-app/tree/main/manhunt-logic):
6464+ Game and lobby logic for the app
6565+- [manhunt-transport/](https://github.com/Bwc9876/manhunt-app/tree/main/manhunt-transport):
6666+ Transport (networking) implementation for communication between apps
6767+- [manhunt-app/](https://github.com/Bwc9876/manhunt-app/tree/main/manhunt-app): App
6368 backend, Rust side of the Tauri application
6469- [frontend/](https://github.com/Bwc9876/manhunt-app/tree/main/frontend): App
6570 frontend, Web side of the Tauri application
···74797580- `just fmt`: Formats all files in the repo
7681- `just check-rust`: Check (and fix) potential issues with Rust code
7777- (only need to run if you edited the backend or signaling)
8282+ (only need to run if you edited rust code)
7883- `just check-frontend`: Check for potential issues on the frontend
7984 (only need to run if you edited the frontend)
80858181-**Important**: When changing any type in `backend` that derives `specta::Type`,
8686+**Important**: When changing any type in a rust file that derives `specta::Type`,
8287you need to run `just export-types` to sync these type bindings to the frontend.
8388Otherwise the TypeScript definitions will not match the ones that the backend expects.
8489
+8-4
TODO.md
···2323- [x] Transport : Handle transport cancellation better
2424- [x] Backend : Add checks for when the `powerup_locations` field is an empty array in settings
2525- [ ] Backend : More tests
2626- - [ ] Lobby tests
2727- - [ ] Game end test for actual return from loop
2828- - [ ] Testing crate for integration testing from a DSL
2929- - [ ] NixOS VM tests wrapping the testing crate
2626+ - [x] Lobby tests
2727+ - [x] Game end test for actual return from loop
2828+ - [x] More transport crate tests
2929+ - [x] Signaling is wrong, only kick everyone else on host leave if the lobby is open
3030+ - [x] Organize signalling and seperate out more logic
3131+ - [x] Signaling tests
3232+ - [ ] Testing crate for integration testing?
3333+ - [ ] NixOS VM tests wrapping the testing crate?
3034- [ ] Nix : Cheat the dependency nightmare and use crane
3135- [x] Nix : Fix manhunt.nix to actually build
3236- [ ] Frontend : Rework state management, better hooks
backend/.gitignore
manhunt-app/.gitignore
+7-7
backend/Cargo.toml
manhunt-app/Cargo.toml
···33version = "0.1.0"
44description = "A mobile app for playing the game \"manhunt\""
55authors = ["Ben C <bwc9876@gmail.com>"]
66-edition = "2021"
66+edition = "2024"
77default-run = "manhunt-app"
8899[lib]
···2222tauri-plugin-opener = "2"
2323serde = { version = "1", features = ["derive"] }
2424serde_json = "1"
2525-tokio = { version = "1.45", features = ["sync", "macros", "time", "fs"] }
2525+tokio = { version = "1.49", features = ["sync", "macros", "time", "fs"] }
2626tauri-plugin-geolocation = "2"
2727-tauri-plugin-store = "2.2.0"
2727+tauri-plugin-store = "2.4.2"
2828specta = { version = "=2.0.0-rc.22", features = ["chrono", "uuid", "export"] }
2929tauri-specta = { version = "=2.0.0-rc.21", features = ["derive", "typescript"] }
3030specta-typescript = "0.0.9"
3131tauri-plugin-log = "2"
3232tauri-plugin-notification = "2"
3333-log = "0.4.27"
3434-anyhow = "1.0.98"
3333+log = "0.4.29"
3434+anyhow = "1.0.101"
3535tauri-plugin-dialog = "2"
3636manhunt-logic = { version = "0.1.0", path = "../manhunt-logic" }
3737manhunt-transport = { version = "0.1.0", path = "../manhunt-transport" }
3838-uuid = { version = "1.17.0", features = ["serde"] }
3939-chrono = { version = "0.4.41", features = ["serde"] }
3838+uuid = { version = "1.21.0", features = ["serde"] }
3939+chrono = { version = "0.4.43", features = ["serde"] }
···2929 cargo fmt --check
3030 cargo check
3131 cargo clippy --fix --allow-dirty --allow-staged -- -D warnings
3232+ cargo nextest run
32333334# Run lint on the frontend
3435[working-directory('frontend')]
···3637 npm run lint
37383839# Export types from the backend to TypeScript bindings
3939-[working-directory('backend')]
4040export-types:
4141- cargo run --bin export-types ../frontend/src/bindings.ts
4242- prettier --write ../frontend/src/bindings.ts --config ../.prettierrc.yaml
4141+ cargo run --bin export-types frontend/src/bindings.ts
4242+ prettier --write frontend/src/bindings.ts --config .prettierrc.yaml
43434444# Start the signaling server on localhost:3536
4545[working-directory('manhunt-signaling')]
+297
manhunt-app/src/lib.rs
···11+mod history;
22+mod location;
33+mod profiles;
44+mod state;
55+66+use std::collections::HashMap;
77+88+use log::LevelFilter;
99+use manhunt_logic::{GameSettings, GameUiState, LobbyState, PlayerProfile, UtcDT};
1010+use manhunt_transport::room_exists;
1111+use tauri::{AppHandle, Manager, State};
1212+use tauri_specta::{ErrorHandlingMode, collect_commands, collect_events};
1313+use tokio::sync::RwLock;
1414+use uuid::Uuid;
1515+1616+use std::result::Result as StdResult;
1717+1818+use crate::{
1919+ history::AppGameHistory,
2020+ profiles::{read_profile_from_store, write_profile_to_store},
2121+ state::{AppScreen, AppState, AppStateHandle, ChangeScreen, GameStateUpdate, LobbyStateUpdate},
2222+};
2323+2424+type Result<T = (), E = String> = StdResult<T, E>;
2525+2626+// == GENERAL / FLOW COMMANDS ==
2727+2828+#[tauri::command]
2929+#[specta::specta]
3030+/// Get the screen the app should currently be on, returns [AppScreen]
3131+async fn get_current_screen(state: State<'_, AppStateHandle>) -> Result<AppScreen> {
3232+ let state = state.read().await;
3333+ Ok(match &*state {
3434+ AppState::Setup => AppScreen::Setup,
3535+ AppState::Menu(_player_profile) => AppScreen::Menu,
3636+ AppState::Lobby(_lobby) => AppScreen::Lobby,
3737+ AppState::Game(_game, _profiles) => AppScreen::Game,
3838+ AppState::Replay(_) => AppScreen::Replay,
3939+ })
4040+}
4141+4242+#[tauri::command]
4343+#[specta::specta]
4444+/// Quit a running game or leave a lobby
4545+async fn quit_to_menu(app: AppHandle, state: State<'_, AppStateHandle>) -> Result {
4646+ let mut state = state.write().await;
4747+ state.quit_to_menu(app).await;
4848+ Ok(())
4949+}
5050+5151+// == AppState::Setup COMMANDS
5252+5353+#[tauri::command]
5454+#[specta::specta]
5555+/// (Screen: Setup) Complete user setup and go to the menu screen
5656+async fn complete_setup(
5757+ profile: PlayerProfile,
5858+ app: AppHandle,
5959+ state: State<'_, AppStateHandle>,
6060+) -> Result {
6161+ state.write().await.complete_setup(&app, profile)
6262+}
6363+6464+// == AppState::Menu COMMANDS ==
6565+6666+#[tauri::command]
6767+#[specta::specta]
6868+/// (Screen: Menu) Get the user's player profile
6969+async fn get_profile(state: State<'_, AppStateHandle>) -> Result<PlayerProfile> {
7070+ let state = state.read().await;
7171+ let profile = state.get_menu()?;
7272+ Ok(profile.clone())
7373+}
7474+7575+#[tauri::command]
7676+#[specta::specta]
7777+/// (Screen: Menu) Get a list of all previously played games, returns of list of DateTimes that represent when
7878+/// each game started, use this as a key
7979+fn list_game_histories(app: AppHandle) -> Result<Vec<UtcDT>> {
8080+ AppGameHistory::ls_histories(&app)
8181+ .map_err(|err| err.context("Failed to get game histories").to_string())
8282+}
8383+8484+#[tauri::command]
8585+#[specta::specta]
8686+/// (Screen: Menu) Go to the game replay screen to replay the game history specified by id
8787+async fn replay_game(id: UtcDT, app: AppHandle, state: State<'_, AppStateHandle>) -> Result {
8888+ state.write().await.replay_game(&app, id)
8989+}
9090+9191+#[tauri::command]
9292+#[specta::specta]
9393+/// (Screen: Menu) Check if a room code is valid to join, use this before starting a game
9494+/// for faster error checking.
9595+async fn check_room_code(code: &str) -> Result<bool> {
9696+ room_exists(code).await.map_err(|err| err.to_string())
9797+}
9898+9999+#[tauri::command]
100100+#[specta::specta]
101101+/// (Screen: Menu) Update the player's profile and persist it
102102+async fn update_profile(
103103+ new_profile: PlayerProfile,
104104+ app: AppHandle,
105105+ state: State<'_, AppStateHandle>,
106106+) -> Result {
107107+ write_profile_to_store(&app, new_profile.clone());
108108+ let mut state = state.write().await;
109109+ let profile = state.get_menu_mut()?;
110110+ *profile = new_profile;
111111+ Ok(())
112112+}
113113+114114+#[tauri::command]
115115+#[specta::specta]
116116+/// (Screen: Menu) Start/Join a new lobby, set `join_code` to `null` to be host,
117117+/// set it to a join code to be a client. This triggers a screen change to [AppScreen::Lobby]
118118+async fn start_lobby(
119119+ app: AppHandle,
120120+ join_code: Option<String>,
121121+ settings: GameSettings,
122122+ state: State<'_, AppStateHandle>,
123123+) -> Result {
124124+ let mut state = state.write().await;
125125+ state.start_lobby(join_code, app, settings).await;
126126+ Ok(())
127127+}
128128+129129+// AppState::Lobby COMMANDS
130130+131131+#[tauri::command]
132132+#[specta::specta]
133133+/// (Screen: Lobby) Get the current state of the lobby, call after receiving an update event
134134+async fn get_lobby_state(state: State<'_, AppStateHandle>) -> Result<LobbyState> {
135135+ let lobby = state.read().await.get_lobby()?;
136136+ Ok(lobby.clone_state().await)
137137+}
138138+139139+#[tauri::command]
140140+#[specta::specta]
141141+/// (Screen: Lobby) Switch teams between seekers and hiders, returns the new [LobbyState]
142142+async fn switch_teams(seeker: bool, state: State<'_, AppStateHandle>) -> Result {
143143+ let lobby = state.read().await.get_lobby()?;
144144+ lobby.switch_teams(seeker).await;
145145+ Ok(())
146146+}
147147+148148+#[tauri::command]
149149+#[specta::specta]
150150+/// (Screen: Lobby) HOST ONLY: Push new settings to everyone, does nothing on clients. Returns the
151151+/// new lobby state
152152+async fn host_update_settings(settings: GameSettings, state: State<'_, AppStateHandle>) -> Result {
153153+ let lobby = state.read().await.get_lobby()?;
154154+ lobby.update_settings(settings).await;
155155+ Ok(())
156156+}
157157+158158+#[tauri::command]
159159+#[specta::specta]
160160+/// (Screen: Lobby) HOST ONLY: Start the game, stops anyone else from joining and switched screen
161161+/// to AppScreen::Game.
162162+async fn host_start_game(state: State<'_, AppStateHandle>) -> Result {
163163+ state.read().await.get_lobby()?.start_game().await;
164164+ Ok(())
165165+}
166166+167167+// AppScreen::Game COMMANDS
168168+169169+#[tauri::command]
170170+#[specta::specta]
171171+/// (Screen: Game) Get all player profiles with display names and profile pictures for this game.
172172+/// This value will never change and is fairly expensive to clone, so please minimize calls to
173173+/// this command.
174174+async fn get_profiles(state: State<'_, AppStateHandle>) -> Result<HashMap<Uuid, PlayerProfile>> {
175175+ state.read().await.get_profiles().cloned()
176176+}
177177+178178+#[tauri::command]
179179+#[specta::specta]
180180+/// (Screen: Game) Get the current settings for this game.
181181+async fn get_game_settings(state: State<'_, AppStateHandle>) -> Result<GameSettings> {
182182+ Ok(state.read().await.get_game()?.clone_settings().await)
183183+}
184184+185185+#[tauri::command]
186186+#[specta::specta]
187187+/// (Screen: Game) Get the current state of the game.
188188+async fn get_game_state(state: State<'_, AppStateHandle>) -> Result<GameUiState> {
189189+ Ok(state.read().await.get_game()?.get_ui_state().await)
190190+}
191191+192192+#[tauri::command]
193193+#[specta::specta]
194194+/// (Screen: Game) Mark this player as caught, this player will become a seeker. Returns the new game state
195195+async fn mark_caught(state: State<'_, AppStateHandle>) -> Result {
196196+ let game = state.read().await.get_game()?;
197197+ game.mark_caught().await;
198198+ Ok(())
199199+}
200200+201201+#[tauri::command]
202202+#[specta::specta]
203203+/// (Screen: Game) Grab a powerup on the map, this should be called when the user is *in range* of
204204+/// the powerup. Returns the new game state after rolling for the powerup
205205+async fn grab_powerup(state: State<'_, AppStateHandle>) -> Result {
206206+ let game = state.read().await.get_game()?;
207207+ game.get_powerup().await;
208208+ Ok(())
209209+}
210210+211211+#[tauri::command]
212212+#[specta::specta]
213213+/// (Screen: Game) Use the currently held powerup in the player's held_powerup. Does nothing if the
214214+/// player has none. Returns the updated game state
215215+async fn activate_powerup(state: State<'_, AppStateHandle>) -> Result {
216216+ let game = state.read().await.get_game()?;
217217+ game.use_powerup().await;
218218+ Ok(())
219219+}
220220+221221+// AppState::Replay COMMANDS
222222+223223+#[tauri::command]
224224+#[specta::specta]
225225+/// (Screen: Replay) Get the game history that's currently being replayed. Try to limit calls to
226226+/// this
227227+async fn get_current_replay_history(state: State<'_, AppStateHandle>) -> Result<AppGameHistory> {
228228+ state.read().await.get_replay()
229229+}
230230+231231+pub fn mk_specta() -> tauri_specta::Builder {
232232+ tauri_specta::Builder::<tauri::Wry>::new()
233233+ .error_handling(ErrorHandlingMode::Throw)
234234+ .commands(collect_commands![
235235+ start_lobby,
236236+ get_profile,
237237+ quit_to_menu,
238238+ get_current_screen,
239239+ update_profile,
240240+ get_lobby_state,
241241+ host_update_settings,
242242+ switch_teams,
243243+ host_start_game,
244244+ mark_caught,
245245+ grab_powerup,
246246+ activate_powerup,
247247+ check_room_code,
248248+ get_profiles,
249249+ replay_game,
250250+ list_game_histories,
251251+ get_current_replay_history,
252252+ get_game_settings,
253253+ get_game_state,
254254+ complete_setup,
255255+ ])
256256+ .events(collect_events![
257257+ ChangeScreen,
258258+ GameStateUpdate,
259259+ LobbyStateUpdate
260260+ ])
261261+}
262262+263263+#[cfg_attr(mobile, tauri::mobile_entry_point)]
264264+pub fn run() {
265265+ let state = RwLock::new(AppState::Setup);
266266+267267+ let builder = mk_specta();
268268+269269+ tauri::Builder::default()
270270+ .plugin(tauri_plugin_dialog::init())
271271+ .plugin(tauri_plugin_notification::init())
272272+ .plugin(
273273+ tauri_plugin_log::Builder::new()
274274+ .level(LevelFilter::Debug)
275275+ .build(),
276276+ )
277277+ .plugin(tauri_plugin_opener::init())
278278+ .plugin(tauri_plugin_geolocation::init())
279279+ .plugin(tauri_plugin_store::Builder::default().build())
280280+ .invoke_handler(builder.invoke_handler())
281281+ .manage(state)
282282+ .setup(move |app| {
283283+ builder.mount_events(app);
284284+285285+ let handle = app.handle().clone();
286286+ tauri::async_runtime::spawn(async move {
287287+ if let Some(profile) = read_profile_from_store(&handle) {
288288+ let state_handle = handle.state::<AppStateHandle>();
289289+ let mut state = state_handle.write().await;
290290+ *state = AppState::Menu(profile);
291291+ }
292292+ });
293293+ Ok(())
294294+ })
295295+ .run(tauri::generate_context!())
296296+ .expect("error while running tauri application");
297297+}
+300
manhunt-app/src/state.rs
···11+use std::{collections::HashMap, marker::PhantomData, sync::Arc, time::Duration};
22+33+use anyhow::Context;
44+use log::{error, info, warn};
55+use manhunt_logic::{
66+ Game as BaseGame, GameSettings, Lobby as BaseLobby, PlayerProfile, StartGameInfo,
77+ StateUpdateSender, UtcDT,
88+};
99+use manhunt_transport::{MatchboxTransport, request_room_code};
1010+use serde::{Deserialize, Serialize};
1111+use tauri::{AppHandle, Manager};
1212+use tauri_plugin_dialog::{DialogExt, MessageDialogKind};
1313+use tauri_specta::Event;
1414+use tokio::sync::RwLock;
1515+use uuid::Uuid;
1616+1717+use crate::{
1818+ Result,
1919+ history::AppGameHistory,
2020+ location::TauriLocation,
2121+ profiles::{read_profile_from_store, write_profile_to_store},
2222+};
2323+2424+/// The state of the game has changed
2525+#[derive(Serialize, Deserialize, Clone, Default, Debug, specta::Type, tauri_specta::Event)]
2626+pub struct GameStateUpdate;
2727+2828+/// The state of the lobby has changed
2929+#[derive(Serialize, Deserialize, Clone, Default, Debug, specta::Type, tauri_specta::Event)]
3030+pub struct LobbyStateUpdate;
3131+3232+pub struct TauriStateUpdateSender<E: Clone + Default + Event + Serialize>(
3333+ AppHandle,
3434+ PhantomData<E>,
3535+);
3636+3737+impl<E: Serialize + Clone + Default + Event> TauriStateUpdateSender<E> {
3838+ fn new(app: &AppHandle) -> Self {
3939+ Self(app.clone(), PhantomData)
4040+ }
4141+}
4242+4343+impl<E: Serialize + Clone + Default + Event> StateUpdateSender for TauriStateUpdateSender<E> {
4444+ fn send_update(&self) {
4545+ if let Err(why) = E::default().emit(&self.0) {
4646+ error!("Error sending Game state update to UI: {why:?}");
4747+ }
4848+ }
4949+}
5050+5151+type Game = BaseGame<TauriLocation, MatchboxTransport, TauriStateUpdateSender<GameStateUpdate>>;
5252+type Lobby = BaseLobby<MatchboxTransport, TauriStateUpdateSender<LobbyStateUpdate>>;
5353+5454+pub enum AppState {
5555+ Setup,
5656+ Menu(PlayerProfile),
5757+ Lobby(Arc<Lobby>),
5858+ Game(Arc<Game>, HashMap<Uuid, PlayerProfile>),
5959+ Replay(AppGameHistory),
6060+}
6161+6262+#[derive(Serialize, Deserialize, specta::Type, Debug, Clone, Eq, PartialEq)]
6363+pub enum AppScreen {
6464+ Setup,
6565+ Menu,
6666+ Lobby,
6767+ Game,
6868+ Replay,
6969+}
7070+7171+pub type AppStateHandle = RwLock<AppState>;
7272+7373+const GAME_TICK_RATE: Duration = Duration::from_secs(1);
7474+7575+/// The app is changing screens, contains the screen it's switching to
7676+#[derive(Serialize, Deserialize, Clone, Debug, specta::Type, tauri_specta::Event)]
7777+pub struct ChangeScreen(AppScreen);
7878+7979+fn error_dialog(app: &AppHandle, msg: &str) {
8080+ app.dialog()
8181+ .message(msg)
8282+ .kind(MessageDialogKind::Error)
8383+ .show(|_| {});
8484+}
8585+8686+impl AppState {
8787+ pub async fn start_game(&mut self, app: AppHandle, start: StartGameInfo) {
8888+ if let AppState::Lobby(lobby) = self {
8989+ let transport = lobby.clone_transport();
9090+ let profiles = lobby.clone_profiles().await;
9191+ let location = TauriLocation::new(app.clone());
9292+ let state_updates = TauriStateUpdateSender::new(&app);
9393+ let game = Arc::new(Game::new(
9494+ GAME_TICK_RATE,
9595+ start,
9696+ transport,
9797+ location,
9898+ state_updates,
9999+ ));
100100+ *self = AppState::Game(game.clone(), profiles.clone());
101101+ Self::game_loop(app.clone(), game, profiles);
102102+ Self::emit_screen_change(&app, AppScreen::Game);
103103+ }
104104+ }
105105+106106+ fn game_loop(app: AppHandle, game: Arc<Game>, profiles: HashMap<Uuid, PlayerProfile>) {
107107+ tokio::spawn(async move {
108108+ let res = game.main_loop().await;
109109+ let state_handle = app.state::<AppStateHandle>();
110110+ let mut state = state_handle.write().await;
111111+ match res {
112112+ Ok(Some(history)) => {
113113+ let history =
114114+ AppGameHistory::new(history, profiles, game.clone_settings().await);
115115+ if let Err(why) = history.save_history(&app) {
116116+ error!("Failed to save game history: {why:?}");
117117+ error_dialog(&app, "Failed to save the history of this game");
118118+ }
119119+ state.quit_to_menu(app.clone()).await;
120120+ }
121121+ Ok(None) => {
122122+ info!("User quit game");
123123+ }
124124+ Err(why) => {
125125+ error!("Game Error: {why:?}");
126126+ app.dialog()
127127+ .message(format!("Connection Error: {why}"))
128128+ .kind(MessageDialogKind::Error)
129129+ .show(|_| {});
130130+ state.quit_to_menu(app.clone()).await;
131131+ }
132132+ }
133133+ });
134134+ }
135135+136136+ pub fn get_menu(&self) -> Result<&PlayerProfile> {
137137+ match self {
138138+ AppState::Menu(player_profile) => Ok(player_profile),
139139+ _ => Err("Not on menu screen".to_string()),
140140+ }
141141+ }
142142+143143+ pub fn get_menu_mut(&mut self) -> Result<&mut PlayerProfile> {
144144+ match self {
145145+ AppState::Menu(player_profile) => Ok(player_profile),
146146+ _ => Err("Not on menu screen".to_string()),
147147+ }
148148+ }
149149+150150+ pub fn get_lobby(&self) -> Result<Arc<Lobby>> {
151151+ if let AppState::Lobby(lobby) = self {
152152+ Ok(lobby.clone())
153153+ } else {
154154+ Err("Not on lobby screen".to_string())
155155+ }
156156+ }
157157+158158+ pub fn get_game(&self) -> Result<Arc<Game>> {
159159+ if let AppState::Game(game, _) = self {
160160+ Ok(game.clone())
161161+ } else {
162162+ Err("Not on game screen".to_string())
163163+ }
164164+ }
165165+166166+ pub fn get_profiles(&self) -> Result<&HashMap<Uuid, PlayerProfile>> {
167167+ if let AppState::Game(_, profiles) = self {
168168+ Ok(profiles)
169169+ } else {
170170+ Err("Not on game screen".to_string())
171171+ }
172172+ }
173173+174174+ pub fn get_replay(&self) -> Result<AppGameHistory> {
175175+ if let AppState::Replay(history) = self {
176176+ Ok(history.clone())
177177+ } else {
178178+ Err("Not on replay screen".to_string())
179179+ }
180180+ }
181181+182182+ fn emit_screen_change(app: &AppHandle, screen: AppScreen) {
183183+ if let Err(why) = ChangeScreen(screen).emit(app) {
184184+ warn!("Error emitting screen change: {why:?}");
185185+ }
186186+ }
187187+188188+ pub fn complete_setup(&mut self, app: &AppHandle, profile: PlayerProfile) -> Result {
189189+ if let AppState::Setup = self {
190190+ write_profile_to_store(app, profile.clone());
191191+ *self = AppState::Menu(profile);
192192+ Self::emit_screen_change(app, AppScreen::Menu);
193193+ Ok(())
194194+ } else {
195195+ Err("Must be on the Setup screen".to_string())
196196+ }
197197+ }
198198+199199+ pub fn replay_game(&mut self, app: &AppHandle, id: UtcDT) -> Result {
200200+ if let AppState::Menu(_) = self {
201201+ let history = AppGameHistory::get_history(app, id)
202202+ .context("Failed to read history")
203203+ .map_err(|e| e.to_string())?;
204204+ *self = AppState::Replay(history);
205205+ Self::emit_screen_change(app, AppScreen::Replay);
206206+ Ok(())
207207+ } else {
208208+ Err("Not on menu screen".to_string())
209209+ }
210210+ }
211211+212212+ fn lobby_loop(app: AppHandle, lobby: Arc<Lobby>) {
213213+ tokio::spawn(async move {
214214+ let res = lobby.main_loop().await;
215215+ let app_game = app.clone();
216216+ let state_handle = app.state::<AppStateHandle>();
217217+ let mut state = state_handle.write().await;
218218+ match res {
219219+ Ok(Some(start)) => {
220220+ info!("Starting Game");
221221+ state.start_game(app_game, start).await;
222222+ }
223223+ Ok(None) => {
224224+ info!("User quit lobby");
225225+ }
226226+ Err(why) => {
227227+ error!("Lobby Error: {why}");
228228+ error_dialog(&app_game, &format!("Error joining the lobby: {why}"));
229229+ state.quit_to_menu(app_game).await;
230230+ }
231231+ }
232232+ });
233233+ }
234234+235235+ pub async fn start_lobby(
236236+ &mut self,
237237+ join_code: Option<String>,
238238+ app: AppHandle,
239239+ settings: GameSettings,
240240+ ) {
241241+ if let AppState::Menu(profile) = self {
242242+ let host = join_code.is_none();
243243+ let room_code = if let Some(code) = join_code {
244244+ code.to_ascii_uppercase()
245245+ } else {
246246+ match request_room_code().await {
247247+ Ok(code) => code,
248248+ Err(why) => {
249249+ error_dialog(&app, &format!("Couldn't create a lobby\n\n{why:?}"));
250250+ return;
251251+ }
252252+ }
253253+ };
254254+ let state_updates = TauriStateUpdateSender::<LobbyStateUpdate>::new(&app);
255255+ let lobby =
256256+ Lobby::new(&room_code, host, profile.clone(), settings, state_updates).await;
257257+ match lobby {
258258+ Ok(lobby) => {
259259+ *self = AppState::Lobby(lobby.clone());
260260+ Self::lobby_loop(app.clone(), lobby);
261261+ Self::emit_screen_change(&app, AppScreen::Lobby);
262262+ }
263263+ Err(why) => {
264264+ error_dialog(
265265+ &app,
266266+ &format!("Couldn't connect you to the lobby\n\n{why:?}"),
267267+ );
268268+ }
269269+ }
270270+ }
271271+ }
272272+273273+ pub async fn quit_to_menu(&mut self, app: AppHandle) {
274274+ let profile = match self {
275275+ AppState::Setup => None,
276276+ AppState::Menu(_) => {
277277+ warn!("Already on menu!");
278278+ return;
279279+ }
280280+ AppState::Lobby(lobby) => {
281281+ lobby.quit_lobby().await;
282282+ read_profile_from_store(&app)
283283+ }
284284+ AppState::Game(game, _) => {
285285+ game.quit_game().await;
286286+ read_profile_from_store(&app)
287287+ }
288288+ AppState::Replay(_) => read_profile_from_store(&app),
289289+ };
290290+ let screen = if let Some(profile) = profile {
291291+ *self = AppState::Menu(profile);
292292+ AppScreen::Menu
293293+ } else {
294294+ *self = AppState::Setup;
295295+ AppScreen::Setup
296296+ };
297297+298298+ Self::emit_screen_change(&app, screen);
299299+ }
300300+}
+8-7
manhunt-logic/Cargo.toml
···44edition = "2024"
5566[dependencies]
77-anyhow = "1.0.98"
88-chrono = { version = "0.4.41", features = ["serde", "now"] }
99-rand = { version = "0.9.1", features = ["thread_rng"] }
1010-rand_chacha = "0.9.0"
1111-serde = { version = "1.0.219", features = ["derive"] }
77+anyhow = "1.0.101"
88+chrono = { version = "0.4.43", features = ["serde", "now"] }
99+rand = { version = "0.10.0", features = ["thread_rng"] }
1010+rand_chacha = "0.10.0"
1111+serde = { version = "1.0.228", features = ["derive"] }
1212specta = { version = "=2.0.0-rc.22", features = ["uuid", "chrono", "derive"] }
1313-tokio = { version = "1.45.1", features = ["macros", "rt", "sync", "time"] }
1414-uuid = { version = "1.17.0", features = ["serde", "v4"] }
1313+tokio = { version = "1.49.0", features = ["macros", "rt", "sync", "time", "test-util"] }
1414+tokio-util = "0.7.18"
1515+uuid = { version = "1.21.0", features = ["serde", "v4"] }
+155-94
manhunt-logic/src/game.rs
···11use anyhow::bail;
22use chrono::{DateTime, Utc};
33use std::{sync::Arc, time::Duration};
44+use tokio_util::sync::CancellationToken;
45use uuid::Uuid;
5666-use tokio::{sync::RwLock, time::MissedTickBehavior};
77+use tokio::sync::{RwLock, RwLockWriteGuard};
7889use crate::StartGameInfo;
910use crate::{prelude::*, transport::TransportMessage};
···3536 location: L,
3637 state_update_sender: S,
3738 interval: Duration,
3939+ cancel: CancellationToken,
3840}
39414042impl<L: LocationService, T: Transport, S: StateUpdateSender> Game<L, T, S> {
···5759 interval,
5860 state: RwLock::new(state),
5961 state_update_sender,
6262+ cancel: CancellationToken::new(),
6063 }
6164 }
6265···7174 state.remove_ping(id);
7275 // TODO: Maybe reroll for new powerups (specifically seeker ones) instead of just erasing it
7376 state.use_powerup();
7474-7575- self.send_event(GameEvent::PlayerCaught(state.id)).await;
7777+ drop(state);
7878+ self.send_event(GameEvent::PlayerCaught(id)).await;
7679 }
77807881 pub async fn clone_settings(&self) -> GameSettings {
···250253 false
251254 }
252255256256+ pub async fn quit_game(&self) {
257257+ self.cancel.cancel();
258258+ }
259259+253260 #[cfg(test)]
254254- pub async fn force_tick(&self, now: UtcDT) {
255255- let mut state = self.state.write().await;
256256- self.tick(&mut state, now).await;
261261+ fn get_now() -> UtcDT {
262262+ let fake = tokio::time::Instant::now();
263263+ let real = std::time::Instant::now();
264264+ Utc::now() + (fake.into_std().duration_since(real) + Duration::from_secs(1))
257265 }
258266259259- pub async fn quit_game(&self) {
260260- self.transport.disconnect().await;
267267+ #[cfg(not(test))]
268268+ fn get_now() -> UtcDT {
269269+ Utc::now()
261270 }
262271263272 /// Main loop of the game, handles ticking and receiving messages from [Transport].
264273 pub async fn main_loop(&self) -> Result<Option<GameHistory>> {
265274 let mut interval = tokio::time::interval(self.interval);
266275267267- interval.set_missed_tick_behavior(MissedTickBehavior::Delay);
276276+ // interval.set_missed_tick_behavior(MissedTickBehavior::Delay);
268277269278 let res = 'game: loop {
270279 tokio::select! {
271280 biased;
272281282282+ _ = self.cancel.cancelled() => {
283283+ break 'game Ok(None);
284284+ }
285285+273286 messages = self.transport.receive_messages() => {
274287 let mut state = self.state.write().await;
275288 for (id, msg) in messages {
···286299287300 _ = interval.tick() => {
288301 let mut state = self.state.write().await;
289289- let should_break = self.tick(&mut state, Utc::now()).await;
302302+ let should_break = self.tick(&mut state, Self::get_now()).await;
290303291304 if should_break {
292305 let history = state.as_game_history();
···300313301314 res
302315 }
316316+317317+ pub async fn lock_state(&self) -> RwLockWriteGuard<'_, GameState> {
318318+ self.state.write().await
319319+ }
303320}
304321305322#[cfg(test)]
···313330 };
314331315332 use super::*;
316316- use tokio::{task::yield_now, test};
333333+ use tokio::{sync::oneshot, task::yield_now, test};
317334318335 type TestGame = Game<MockLocation, MockTransport, DummySender>;
319336337337+ type EndRecv = oneshot::Receiver<Result<Option<GameHistory>>>;
338338+320339 struct MockMatch {
321340 uuids: Vec<Uuid>,
322322- games: HashMap<u32, Arc<TestGame>>,
341341+ games: Vec<Arc<TestGame>>,
323342 settings: GameSettings,
324324- mock_now: UtcDT,
325343 }
326344327327- const INTERVAL: Duration = Duration::from_secs(u64::MAX);
345345+ const INTERVAL: Duration = Duration::from_secs(600000);
328346329347 impl MockMatch {
330348 pub fn new(settings: GameSettings, players: u32, seekers: u32) -> Self {
349349+ tokio::time::pause();
331350 let (uuids, transports) = MockTransport::create_mesh(players);
332351333352 let initial_caught_state = (0..players)
···336355337356 let games = transports
338357 .into_iter()
339339- .enumerate()
340340- .map(|(id, transport)| {
358358+ .map(|transport| {
341359 let location = MockLocation;
342360 let start_info = StartGameInfo {
343361 initial_caught_state: initial_caught_state.clone(),
···351369 DummySender,
352370 );
353371354354- (id as u32, Arc::new(game))
372372+ Arc::new(game)
355373 })
356356- .collect::<HashMap<_, _>>();
374374+ .collect();
357375358376 Self {
359377 settings,
360378 games,
361379 uuids,
362362- mock_now: Utc::now(),
363380 }
364381 }
365382366366- pub async fn start(&self) {
367367- for game in self.games.values() {
383383+ pub async fn start(&self) -> Vec<EndRecv> {
384384+ let mut recvs = Vec::with_capacity(self.games.len());
385385+ for game in self.games.iter() {
368386 let game = game.clone();
387387+ let (send, recv) = oneshot::channel();
388388+ recvs.push(recv);
369389 tokio::spawn(async move {
370370- game.main_loop().await.expect("Game Start Fail");
390390+ let res = game.main_loop().await;
391391+ send.send(res).expect("Failed to send");
371392 });
372393 yield_now().await;
373394 }
374374- }
375375-376376- pub async fn pass_time(&mut self, d: Duration) {
377377- self.mock_now += d;
395395+ recvs
378396 }
379397380380- pub async fn assert_all_states(&self, f: impl Fn(&GameState)) {
381381- for game in self.games.values() {
398398+ pub async fn assert_all_states(&self, f: impl Fn(usize, &GameState)) {
399399+ for (i, game) in self.games.iter().enumerate() {
382400 let state = game.state.read().await;
383383- f(&state);
401401+ f(i, &state);
384402 }
385403 }
386404387387- pub fn game(&self, id: u32) -> &TestGame {
388388- self.games.get(&id).as_ref().unwrap()
405405+ pub fn assert_all_transports_disconnected(&self) {
406406+ for game in self.games.iter() {
407407+ assert!(
408408+ game.transport.is_disconnected(),
409409+ "Game {} is still connected",
410410+ game.transport.self_id()
411411+ );
412412+ }
389413 }
390414391415 pub async fn wait_for_seekers(&mut self) {
392416 let hiding_time = Duration::from_secs(self.settings.hiding_time_seconds as u64 + 1);
393393- self.mock_now += hiding_time;
417417+418418+ tokio::time::sleep(hiding_time).await;
394419395420 self.tick().await;
396421397397- self.assert_all_states(|s| {
398398- assert!(s.seekers_released());
422422+ self.assert_all_states(|i, s| {
423423+ assert!(s.seekers_released(), "Seekers not released on game {i}");
399424 })
400425 .await;
401426 }
402427403403- async fn tick_all(&self, now: UtcDT) {
404404- for game in self.games.values() {
405405- game.force_tick(now).await;
428428+ pub async fn wait_for_transports(&self) {
429429+ for game in self.games.iter() {
430430+ game.transport.wait_for_queue_empty().await;
406431 }
407432 }
408433409434 pub async fn tick(&self) {
410410- self.tick_all(self.mock_now).await;
435435+ tokio::time::sleep(INTERVAL + Duration::from_secs(1)).await;
436436+ self.wait_for_transports().await;
411437 yield_now().await;
412438 }
413439 }
···436462 // 2 players, one is a seeker
437463 let mut mat = MockMatch::new(settings, 2, 1);
438464439439- mat.start().await;
465465+ let recvs = mat.start().await;
440466441467 mat.wait_for_seekers().await;
442468443443- mat.game(1).mark_caught().await;
469469+ mat.games[1].mark_caught().await;
444470445445- mat.tick().await;
471471+ mat.wait_for_transports().await;
446472447447- mat.assert_all_states(|s| {
473473+ mat.assert_all_states(|i, s| {
448474 assert_eq!(
449475 s.get_caught(mat.uuids[1]),
450476 Some(true),
451451- "Game {} sees player 1 as not caught",
452452- s.id
477477+ "Game {i} sees player 1 as not caught",
453478 );
454479 })
455480 .await;
456481457457- // Extra tick for post-game syncing
482482+ // Tick to process game end
483483+ mat.tick().await;
484484+485485+ mat.assert_all_states(|i, s| {
486486+ assert!(s.game_ended(), "Game {i} has not ended");
487487+ })
488488+ .await;
489489+490490+ // Tick for post-game sync
458491 mat.tick().await;
459492460460- mat.assert_all_states(|s| assert!(s.game_ended(), "Game {} has not ended", s.id))
461461- .await;
493493+ mat.assert_all_transports_disconnected();
494494+495495+ for (i, recv) in recvs.into_iter().enumerate() {
496496+ let res = recv.await.expect("Failed to recv");
497497+ match res {
498498+ Ok(Some(hist)) => {
499499+ assert!(!hist.locations.is_empty(), "Game {i} has no locations");
500500+ assert!(!hist.events.is_empty(), "Game {i} has no event");
501501+ }
502502+ Ok(None) => {
503503+ panic!("Game {i} exited without a history (did not end via post game sync)");
504504+ }
505505+ Err(why) => {
506506+ panic!("Game {i} encountered error: {why:?}");
507507+ }
508508+ }
509509+ }
462510 }
463511464512 #[test]
···472520473521 mat.wait_for_seekers().await;
474522475475- mat.assert_all_states(|s| {
523523+ mat.assert_all_states(|i, s| {
476524 for id in 0..4 {
477525 let ping = s.get_ping(mat.uuids[id]);
478526 if id == 0 {
479527 assert!(
480528 ping.is_none(),
481481- "Game 0 is a seeker and shouldn't be pinged (in {})",
482482- s.id
529529+ "Game {i} has a ping for 0, despite them being a seeker",
483530 );
484531 } else {
485532 assert!(
486533 ping.is_some(),
487487- "Game {} is a hider and should be pinged (in {})",
488488- id,
489489- s.id
534534+ "Game {i} doesn't have a ping for {id}, despite them being a hider",
490535 );
491536 }
492537 }
493538 })
494539 .await;
495540496496- mat.game(1).mark_caught().await;
541541+ mat.games[1].mark_caught().await;
497542498543 mat.tick().await;
499544500500- mat.assert_all_states(|s| {
545545+ mat.assert_all_states(|i, s| {
501546 for id in 0..4 {
502547 let ping = s.get_ping(mat.uuids[id]);
503548 if id <= 1 {
504549 assert!(
505550 ping.is_none(),
506506- "Game {} is a seeker and shouldn't be pinged (in {})",
507507- id,
508508- s.id
551551+ "Game {i} has a ping for {id}, despite them being a seeker",
509552 );
510553 } else {
511554 assert!(
512555 ping.is_some(),
513513- "Game {} is a hider and should be pinged (in {})",
514514- id,
515515- s.id
556556+ "Game {i} doesn't have a ping for {id}, despite them being a hider",
516557 );
517558 }
518559 }
···539580 mat.start().await;
540581 mat.tick().await;
541582 mat.wait_for_seekers().await;
542542- mat.pass_time(Duration::from_secs(60)).await;
583583+ tokio::time::sleep(Duration::from_secs(60)).await;
543584 mat.tick().await;
544585545545- let game = mat.game(0);
586586+ let game = mat.games[0].clone();
546587 let state = game.state.read().await;
547588 let location = state.powerup_location().expect("Powerup didn't spawn");
548589549590 drop(state);
550591551551- mat.assert_all_states(|s| {
592592+ mat.assert_all_states(|i, s| {
552593 assert_eq!(
553594 s.powerup_location(),
554595 Some(location),
555555- "Game {} has a different location than 0",
556556- s.id
596596+ "Game {i} has a different location than 0",
557597 );
558598 })
559599 .await;
···562602 #[test]
563603 async fn test_powerup_ping_seeker_as_you() {
564604 let mut settings = mk_settings();
565565- settings.ping_minutes_interval = 0;
605605+ settings.ping_minutes_interval = 1;
566606 let mut mat = MockMatch::new(settings, 2, 1);
567607568608 mat.start().await;
569609 mat.wait_for_seekers().await;
570610571571- let game = mat.game(1);
611611+ mat.tick().await;
612612+613613+ tokio::time::sleep(Duration::from_secs(60)).await;
614614+615615+ let game = mat.games[1].clone();
572616 let mut state = game.state.write().await;
573617 state.force_set_powerup(PowerUpType::PingSeeker);
574618 drop(state);
575619576620 mat.tick().await;
577621578578- mat.assert_all_states(|s| {
622622+ mat.assert_all_states(|i, s| {
579623 if let Some(ping) = s.get_ping(mat.uuids[1]) {
580624 assert_eq!(
581625 ping.real_player, mat.uuids[0],
582582- "Ping for 1 is not truly 0 (in {})",
583583- s.id
626626+ "Game {i} has a ping for 1, but it wasn't from 0"
584627 );
585628 } else {
586586- panic!("No ping for 1 (in {})", s.id);
629629+ panic!("Game {i} has no ping for 1");
587630 }
588631 })
589632 .await;
···591634592635 #[test]
593636 async fn test_powerup_ping_random_hider() {
594594- let settings = mk_settings();
637637+ let mut settings = mk_settings();
638638+ settings.ping_minutes_interval = u32::MAX;
595639596640 let mut mat = MockMatch::new(settings, 3, 1);
597641598642 mat.start().await;
599643 mat.wait_for_seekers().await;
600644601601- let game = mat.game(1);
645645+ let game = mat.games[1].clone();
602646 let mut state = game.state.write().await;
603647 state.force_set_powerup(PowerUpType::ForcePingOther);
604648 drop(state);
···606650 game.use_powerup().await;
607651 mat.tick().await;
608652609609- mat.assert_all_states(|s| {
610610- // Player 0 is a seeker, player 1 user the powerup, so 2 is the only one that should
611611- // could have pinged
612612- assert!(s.get_ping(mat.uuids[2]).is_some());
613613- assert!(s.get_ping(mat.uuids[0]).is_none());
614614- assert!(s.get_ping(mat.uuids[1]).is_none());
653653+ mat.assert_all_states(|i, s| {
654654+ // Player 0 is a seeker, player 1 used the powerup, so 2 is the only one that should
655655+ // have pinged
656656+ assert!(
657657+ s.get_ping(mat.uuids[2]).is_some(),
658658+ "Ping 2 is not present in game {i}"
659659+ );
660660+ assert!(
661661+ s.get_ping(mat.uuids[0]).is_none(),
662662+ "Ping 0 is present in game {i}"
663663+ );
664664+ assert!(
665665+ s.get_ping(mat.uuids[1]).is_none(),
666666+ "Ping 1 is present in game {i}"
667667+ );
615668 })
616669 .await;
617670 }
···624677625678 mat.start().await;
626679627627- let game = mat.game(3);
680680+ mat.tick().await;
681681+682682+ let game = mat.games[3].clone();
628683 let mut state = game.state.write().await;
629684 state.force_set_powerup(PowerUpType::PingAllSeekers);
630685 drop(state);
631686632687 game.use_powerup().await;
688688+ // One tick to send out the ForcePing
689689+ mat.tick().await;
690690+ // One tick to for the seekers to reply
633691 mat.tick().await;
634692635635- mat.assert_all_states(|s| {
693693+ mat.assert_all_states(|i, s| {
636694 for id in 0..3 {
637695 assert!(
638638- s.get_caught(mat.uuids[id]).is_some(),
639639- "Player {} should be pinged due to the powerup (in {})",
640640- id,
641641- s.id
696696+ &s.get_ping(mat.uuids[id]).is_some(),
697697+ "Game {i} does not have a ping for {id}, despite the powerup being active",
642698 );
643699 }
644700 })
···650706 let settings = mk_settings();
651707 let mat = MockMatch::new(settings, 4, 1);
652708653653- mat.start().await;
709709+ let mut recvs = mat.start().await;
654710655655- let game = mat.game(2);
711711+ let game = mat.games[2].clone();
712712+ let id = game.state.read().await.id;
713713+656714 game.quit_game().await;
657657- let id = game.state.read().await.id;
715715+ let res = recvs.swap_remove(2).await.expect("Failed to recv");
716716+ assert!(res.is_ok_and(|o| o.is_none()), "2 did not exit cleanly");
717717+ assert!(
718718+ game.transport.is_disconnected(),
719719+ "2's transport is not disconnected"
720720+ );
658721659722 mat.tick().await;
660723661661- mat.assert_all_states(|s| {
724724+ mat.assert_all_states(|i, s| {
662725 if s.id != id {
663726 assert!(
664727 s.get_ping(id).is_none(),
665665- "Game {} has not removed 2 from pings",
666666- s.id
728728+ "Game {i} has not removed 2 from pings",
667729 );
668730 assert!(
669731 s.get_caught(id).is_none(),
670670- "Game {} has not removed 2 from caught state",
671671- s.id
732732+ "Game {i} has not removed 2 from caught state",
672733 );
673734 }
674735 })
···1010mod tests;
1111mod transport;
12121313-pub use game::{Game, StateUpdateSender};
1313+pub use game::{Game, StateUpdateSender, UtcDT};
1414pub use game_events::GameEvent;
1515pub use game_state::{GameHistory, GameUiState};
1616pub use lobby::{Lobby, LobbyMessage, LobbyState, StartGameInfo};
1717pub use location::{Location, LocationService};
1818+pub use powerups::PowerUpType;
1819pub use profile::PlayerProfile;
1920pub use settings::GameSettings;
2021pub use transport::{MsgPair, Transport, TransportMessage};
+366-18
manhunt-logic/src/lobby.rs
···33use anyhow::anyhow;
44use serde::{Deserialize, Serialize};
55use tokio::sync::Mutex;
66+use tokio_util::sync::CancellationToken;
67use uuid::Uuid;
7889use crate::{
···3132 PlayerSwitch(Uuid, bool),
3233}
33343434-#[derive(Clone, Serialize, Deserialize, specta::Type)]
3535+#[derive(Debug, Clone, Serialize, Deserialize, specta::Type)]
3536pub struct LobbyState {
3637 profiles: HashMap<Uuid, PlayerProfile>,
3738 join_code: String,
···4849 state: Mutex<LobbyState>,
4950 transport: Arc<T>,
5051 state_updates: U,
5252+ cancel: CancellationToken,
5153}
52545355impl<T: Transport, U: StateUpdateSender> Lobby<T, U> {
5456 pub async fn new(
5557 join_code: &str,
5656- host: bool,
5858+ is_host: bool,
5759 profile: PlayerProfile,
5860 settings: GameSettings,
5961 state_updates: U,
6062 ) -> Result<Arc<Self>> {
6161- let transport = T::initialize(join_code, host)
6363+ let transport = T::initialize(join_code, is_host)
6264 .await
6365 .context("Failed to connect to lobby")?;
64666565- let self_id = transport.self_id();
6767+ let lobby = Arc::new(Self::new_with_transport(
6868+ join_code,
6969+ is_host,
7070+ profile,
7171+ settings,
7272+ state_updates,
7373+ transport,
7474+ ));
66756767- let lobby = Arc::new(Self {
7676+ Ok(lobby)
7777+ }
7878+7979+ pub fn new_with_transport(
8080+ join_code: &str,
8181+ is_host: bool,
8282+ profile: PlayerProfile,
8383+ settings: GameSettings,
8484+ state_updates: U,
8585+ transport: Arc<T>,
8686+ ) -> Self {
8787+ let self_id = transport.self_id();
8888+ Self {
6889 transport,
6990 state_updates,
7070- is_host: host,
9191+ is_host,
9292+ cancel: CancellationToken::new(),
7193 join_code: join_code.to_string(),
7294 state: Mutex::new(LobbyState {
7395 teams: HashMap::from_iter([(self_id, false)]),
7496 join_code: join_code.to_string(),
7597 profiles: HashMap::from_iter([(self_id, profile)]),
7698 self_id,
7777- is_host: host,
9999+ is_host,
78100 settings,
79101 }),
8080- });
8181-8282- Ok(lobby)
102102+ }
83103 }
8410485105 fn emit_state_update(&self) {
···134154 .await
135155 .map(|start_game| Ok(Some(start_game))),
136156 TransportMessage::PeerConnect(peer) => {
137137- let mut state = self.state.lock().await;
138138- state.teams.insert(peer, false);
157157+ let state = self.state.lock().await;
139158 let id = state.self_id;
140159 let msg = LobbyMessage::PlayerSync(id, state.profiles[&id].clone());
160160+ let msg2 = LobbyMessage::PlayerSwitch(id, state.teams[&id]);
141161 drop(state);
142162 self.send_transport_message(Some(peer), msg).await;
163163+ self.send_transport_message(Some(peer), msg2).await;
143164 if self.is_host {
144165 let state = self.state.lock().await;
145166 let msg = LobbyMessage::HostPush(state.settings.clone());
···163184 let res = 'lobby: loop {
164185 self.emit_state_update();
165186166166- let msgs = self.transport.receive_messages().await;
187187+ tokio::select! {
188188+ biased;
167189168168- for (peer, msg) in msgs {
169169- if let Some(res) = self.handle_message(peer, msg).await {
170170- break 'lobby res;
190190+ msgs = self.transport.receive_messages() => {
191191+ for (peer, msg) in msgs {
192192+ if let Some(res) = self.handle_message(peer, msg).await {
193193+ break 'lobby res;
194194+ }
195195+ }
196196+ }
197197+198198+ _ = self.cancel.cancelled() => {
199199+ break Ok(None);
171200 }
172201 }
173202 };
174203175175- if res.is_err() {
204204+ if let Ok(None) | Err(_) = res {
176205 self.transport.disconnect().await;
177206 }
178207···234263 }
235264236265 pub async fn quit_lobby(&self) {
237237- self.transport.disconnect().await;
266266+ self.cancel.cancel();
267267+ }
268268+}
269269+270270+#[cfg(test)]
271271+mod tests {
272272+ use super::*;
273273+ use std::sync::Arc;
274274+ use tokio::{sync::oneshot, task::yield_now, test};
275275+276276+ use crate::tests::{DummySender, MockTransport};
277277+278278+ type MockLobby = Lobby<MockTransport, DummySender>;
279279+280280+ type CompleteRecv = oneshot::Receiver<Result<Option<StartGameInfo>>>;
281281+282282+ struct MockLobbyPool {
283283+ uuids: Vec<Uuid>,
284284+ lobbies: Vec<Arc<MockLobby>>,
285285+ }
286286+287287+ impl MockLobbyPool {
288288+ pub fn new(num_players: u32) -> Self {
289289+ let settings = GameSettings::default();
290290+ let (uuids, transports) = MockTransport::create_mesh(num_players);
291291+292292+ let lobbies = transports
293293+ .into_iter()
294294+ .enumerate()
295295+ .map(|(i, transport)| {
296296+ let profile = PlayerProfile {
297297+ display_name: format!("Lobby {i} ({})", uuids[i]),
298298+ pfp_base64: None,
299299+ };
300300+301301+ Arc::new(MockLobby::new_with_transport(
302302+ "aaa",
303303+ i == 0,
304304+ profile,
305305+ settings.clone(),
306306+ DummySender,
307307+ Arc::new(transport),
308308+ ))
309309+ })
310310+ .collect();
311311+312312+ Self { uuids, lobbies }
313313+ }
314314+315315+ pub async fn wait(&self) {
316316+ for lobby in self.lobbies.iter() {
317317+ lobby.transport.wait_for_queue_empty().await;
318318+ }
319319+ yield_now().await;
320320+ }
321321+322322+ pub async fn start_all_loops(&self) -> Vec<CompleteRecv> {
323323+ let mut recv_set = Vec::with_capacity(self.lobbies.len());
324324+ for lobby in self.lobbies.iter() {
325325+ let lobby = lobby.clone();
326326+ let (send, recv) = oneshot::channel();
327327+ recv_set.push(recv);
328328+ tokio::spawn(async move {
329329+ let res = lobby.main_loop().await;
330330+ send.send(res).ok();
331331+ });
332332+ }
333333+ recv_set
334334+ }
335335+336336+ pub async fn player_join(&self, i: usize) {
337337+ self.lobbies[i].transport.fake_join().await;
338338+ }
339339+340340+ pub async fn assert_state(&self, i: usize, f: impl Fn(&LobbyState)) {
341341+ let state = self.lobbies[i].state.lock().await;
342342+ f(&state);
343343+ }
344344+345345+ pub async fn assert_all_states(&self, f: impl Fn(usize, &LobbyState)) {
346346+ for (i, lobby) in self.lobbies.iter().enumerate() {
347347+ let state = lobby.state.lock().await;
348348+ f(i, &state);
349349+ }
350350+ }
351351+ }
352352+353353+ #[test]
354354+ async fn test_joins() {
355355+ let mat = MockLobbyPool::new(3);
356356+357357+ mat.start_all_loops().await;
358358+359359+ mat.player_join(0).await;
360360+ mat.player_join(1).await;
361361+362362+ mat.wait().await;
363363+364364+ for i in 0..=1 {
365365+ for j in 0..=1 {
366366+ mat.assert_state(i, |s| {
367367+ assert!(
368368+ s.teams.contains_key(&mat.uuids[j]),
369369+ "{i} doesn't have {j}'s uuid in teams"
370370+ );
371371+ assert!(
372372+ s.profiles.contains_key(&mat.uuids[j]),
373373+ "{i} doesn't have {j}'s uuid in profiles"
374374+ );
375375+ })
376376+ .await;
377377+ }
378378+ }
379379+380380+ mat.lobbies[0].switch_teams(true).await;
381381+382382+ mat.wait().await;
383383+384384+ mat.player_join(2).await;
385385+386386+ mat.wait().await;
387387+388388+ mat.assert_all_states(|i, s| {
389389+ for j in 0..=2 {
390390+ assert!(
391391+ s.teams.contains_key(&mat.uuids[j]),
392392+ "{i} doesn't have {j}'s uuid in teams"
393393+ );
394394+ assert!(
395395+ s.profiles.contains_key(&mat.uuids[j]),
396396+ "{i} doesn't have {j}'s uuid in profiles"
397397+ );
398398+ assert_eq!(
399399+ s.teams.get(&mat.uuids[0]).copied(),
400400+ Some(true),
401401+ "{i} doesn't see 0 as a seeker"
402402+ )
403403+ }
404404+ })
405405+ .await;
406406+ }
407407+408408+ #[test]
409409+ async fn test_team_switch() {
410410+ let mat = MockLobbyPool::new(3);
411411+412412+ mat.start_all_loops().await;
413413+414414+ mat.lobbies[2].switch_teams(true).await;
415415+416416+ mat.wait().await;
417417+418418+ mat.assert_all_states(|i, s| {
419419+ assert_eq!(
420420+ s.teams.get(&mat.uuids[2]).copied(),
421421+ Some(true),
422422+ "{i} ({}) does not see 2 as a seeker",
423423+ mat.uuids[i]
424424+ );
425425+ })
426426+ .await;
427427+ }
428428+429429+ #[test]
430430+ async fn test_update_settings() {
431431+ let mat = MockLobbyPool::new(2);
432432+433433+ mat.start_all_loops().await;
434434+435435+ let mut settings = GameSettings::default();
436436+ const UPDATED_ID: u32 = 284829;
437437+ settings.hiding_time_seconds = UPDATED_ID;
438438+439439+ mat.lobbies[0].update_settings(settings).await;
440440+441441+ mat.wait().await;
442442+443443+ mat.assert_all_states(|i, s| {
444444+ assert_eq!(
445445+ s.settings.hiding_time_seconds, UPDATED_ID,
446446+ "{i} ({}) did not get updated settings",
447447+ mat.uuids[i]
448448+ )
449449+ })
450450+ .await;
451451+ }
452452+453453+ #[test]
454454+ async fn test_update_settings_not_host() {
455455+ let mat = MockLobbyPool::new(2);
456456+457457+ mat.start_all_loops().await;
458458+459459+ let mut settings = GameSettings::default();
460460+ let target = settings.hiding_time_seconds;
461461+ const UPDATED_ID: u32 = 284829;
462462+ settings.hiding_time_seconds = UPDATED_ID;
463463+464464+ mat.lobbies[1].update_settings(settings).await;
465465+466466+ mat.wait().await;
467467+468468+ mat.assert_all_states(|i, s| {
469469+ assert_eq!(
470470+ s.settings.hiding_time_seconds, target,
471471+ "{i} ({}) updated settings despite 1 not being host",
472472+ mat.uuids[i]
473473+ )
474474+ })
475475+ .await;
476476+ }
477477+478478+ #[test]
479479+ async fn test_game_start() {
480480+ let mat = MockLobbyPool::new(4);
481481+482482+ let recvs = mat.start_all_loops().await;
483483+484484+ for i in 0..3 {
485485+ mat.player_join(i).await;
486486+ }
487487+488488+ mat.lobbies[2].switch_teams(true).await;
489489+490490+ let settings = GameSettings {
491491+ hiding_time_seconds: 45,
492492+ ..Default::default()
493493+ };
494494+495495+ mat.lobbies[0].update_settings(settings).await;
496496+497497+ mat.lobbies[3].quit_lobby().await;
498498+499499+ mat.wait().await;
500500+501501+ mat.lobbies[0].start_game().await;
502502+503503+ mat.wait().await;
504504+505505+ for (i, recv) in recvs.into_iter().enumerate() {
506506+ let res = recv.await.expect("Failed to recv");
507507+ match res {
508508+ Ok(Some(StartGameInfo {
509509+ settings,
510510+ initial_caught_state,
511511+ })) => {
512512+ assert_eq!(
513513+ settings.hiding_time_seconds, 45,
514514+ "Lobby {i} does not match pushed settings"
515515+ );
516516+ assert_eq!(
517517+ initial_caught_state.len(),
518518+ 3,
519519+ "Lobby {i} does not have 3 entries in caught state"
520520+ );
521521+ assert_eq!(
522522+ initial_caught_state.get(&mat.uuids[2]).copied(),
523523+ Some(true),
524524+ "Lobby {i} does not see 2 as a seeker"
525525+ );
526526+ assert!(
527527+ initial_caught_state.keys().all(|id| *id != mat.uuids[3]),
528528+ "Lobby {i} still has a disconnected player saved in caught state"
529529+ );
530530+ let profiles = mat.lobbies[i].clone_profiles().await;
531531+ assert_eq!(
532532+ profiles.len(),
533533+ 3,
534534+ "Lobby {i} does not have 3 entries in profiles"
535535+ );
536536+ assert!(
537537+ profiles.keys().all(|id| *id != mat.uuids[3]),
538538+ "Lobby {i} still has a disconnected player saved in profiles"
539539+ );
540540+ }
541541+ Ok(None) => {
542542+ if i != 3 {
543543+ panic!("Lobby {i} did not exit with start info");
544544+ }
545545+ }
546546+ Err(why) => {
547547+ panic!("Lobby {i} had an error: {why:?}");
548548+ }
549549+ }
550550+ }
551551+ }
552552+553553+ #[test]
554554+ async fn test_drop_player() {
555555+ let mat = MockLobbyPool::new(3);
556556+557557+ let mut recvs = mat.start_all_loops().await;
558558+559559+ mat.lobbies[1].quit_lobby().await;
560560+561561+ let res = recvs.swap_remove(1).await.expect("Failed to recv");
562562+ assert!(res.is_ok_and(|o| o.is_none()), "1 did not quit gracefully");
563563+564564+ mat.wait().await;
565565+566566+ assert!(
567567+ mat.lobbies[1].transport.is_disconnected(),
568568+ "1 is not disconnected"
569569+ );
570570+571571+ let id = mat.uuids[1];
572572+573573+ mat.assert_all_states(|i, s| {
574574+ if mat.uuids[i] != id {
575575+ assert!(
576576+ !s.teams.contains_key(&id),
577577+ "{i} has not been removed 1 from teams"
578578+ );
579579+ assert!(
580580+ !s.profiles.contains_key(&id),
581581+ "{i} has not been removed 1 from profiles"
582582+ );
583583+ }
584584+ })
585585+ .await;
238586 }
239587}
···11+#![allow(clippy::result_large_err)]
22+33+use manhunt_logic::{
44+ Game as BaseGame, GameSettings, Lobby as BaseLobby, Location, LocationService, PlayerProfile,
55+ StartGameInfo, StateUpdateSender,
66+};
77+use manhunt_test_shared::*;
88+use manhunt_transport::{MatchboxTransport, request_room_code};
99+use std::{sync::Arc, time::Duration};
1010+use tokio::{
1111+ io::{AsyncBufReadExt, AsyncWriteExt, BufReader},
1212+ sync::{Mutex, mpsc},
1313+};
1414+1515+struct DummyLocationService;
1616+1717+impl LocationService for DummyLocationService {
1818+ fn get_loc(&self) -> Option<manhunt_logic::Location> {
1919+ Some(Location {
2020+ lat: 0.0,
2121+ long: 0.0,
2222+ heading: None,
2323+ })
2424+ }
2525+}
2626+2727+struct UpdateSender(mpsc::Sender<()>);
2828+2929+impl StateUpdateSender for UpdateSender {
3030+ fn send_update(&self) {
3131+ let tx = self.0.clone();
3232+ tokio::spawn(async move {
3333+ tx.send(()).await.expect("Failed to send");
3434+ });
3535+ }
3636+}
3737+3838+type Game = BaseGame<DummyLocationService, MatchboxTransport, UpdateSender>;
3939+type Lobby = BaseLobby<MatchboxTransport, UpdateSender>;
4040+4141+#[derive(Default)]
4242+enum DaemonScreen {
4343+ #[default]
4444+ PreConnect,
4545+ Lobby(Arc<Lobby>),
4646+ Game(Arc<Game>),
4747+}
4848+4949+impl DaemonScreen {
5050+ pub fn as_update(&self) -> ScreenUpdate {
5151+ match self {
5252+ Self::PreConnect => ScreenUpdate::PreConnect,
5353+ Self::Game(_) => ScreenUpdate::Game,
5454+ Self::Lobby(_) => ScreenUpdate::Lobby,
5555+ }
5656+ }
5757+}
5858+5959+type StateHandle = Arc<Mutex<DaemonState>>;
6060+6161+struct DaemonState {
6262+ screen: DaemonScreen,
6363+ profile: PlayerProfile,
6464+ responses: mpsc::Sender<TestingResponse>,
6565+ updates: (mpsc::Sender<()>, Mutex<mpsc::Receiver<()>>),
6666+}
6767+6868+impl DaemonState {
6969+ pub fn new(name: impl Into<String>, responses: mpsc::Sender<TestingResponse>) -> Self {
7070+ tokio::time::pause();
7171+ let screen = DaemonScreen::default();
7272+ let (tx, rx) = mpsc::channel(2);
7373+ Self {
7474+ screen,
7575+ responses,
7676+ profile: PlayerProfile {
7777+ display_name: name.into(),
7878+ pfp_base64: None,
7979+ },
8080+ updates: (tx, Mutex::new(rx)),
8181+ }
8282+ }
8383+8484+ async fn change_screen(&mut self, new_screen: DaemonScreen) {
8585+ let update = new_screen.as_update();
8686+ self.screen = new_screen;
8787+ self.push_resp(update).await;
8888+ }
8989+9090+ async fn lobby_loop(&self, handle: StateHandle) {
9191+ if let DaemonScreen::Lobby(lobby) = &self.screen {
9292+ let lobby = lobby.clone();
9393+ tokio::spawn(async move {
9494+ let res = lobby.main_loop().await;
9595+ let handle2 = handle.clone();
9696+ let mut state = handle.lock().await;
9797+ match res {
9898+ Ok(Some(start)) => {
9999+ state.start_game(handle2, start).await;
100100+ }
101101+ Ok(None) => {
102102+ state.change_screen(DaemonScreen::PreConnect).await;
103103+ }
104104+ Err(why) => {
105105+ state.push_resp(why).await;
106106+ state.change_screen(DaemonScreen::PreConnect).await;
107107+ }
108108+ }
109109+ });
110110+ }
111111+ }
112112+113113+ async fn game_loop(&self, handle: StateHandle) {
114114+ if let DaemonScreen::Game(game) = &self.screen {
115115+ let game = game.clone();
116116+ tokio::spawn(async move {
117117+ let res = game.main_loop().await;
118118+ let mut state = handle.lock().await;
119119+ match res {
120120+ Ok(Some(history)) => {
121121+ state.push_resp(history).await;
122122+ }
123123+ Ok(None) => {}
124124+ Err(why) => {
125125+ state.push_resp(why).await;
126126+ }
127127+ }
128128+ state.change_screen(DaemonScreen::PreConnect).await;
129129+ });
130130+ }
131131+ }
132132+133133+ async fn push_resp(&self, resp: impl Into<TestingResponse>) {
134134+ self.responses
135135+ .send(resp.into())
136136+ .await
137137+ .expect("Failed to push response");
138138+ }
139139+140140+ fn sender(&self) -> UpdateSender {
141141+ UpdateSender(self.updates.0.clone())
142142+ }
143143+144144+ const INTERVAL: Duration = Duration::from_secs(1);
145145+146146+ async fn start_game(&mut self, handle: StateHandle, start: StartGameInfo) {
147147+ if let DaemonScreen::Lobby(lobby) = &self.screen {
148148+ let transport = lobby.clone_transport();
149149+ let updates = self.sender();
150150+ let location = DummyLocationService;
151151+152152+ let game = Game::new(Self::INTERVAL, start, transport, location, updates);
153153+154154+ self.change_screen(DaemonScreen::Game(Arc::new(game))).await;
155155+ self.game_loop(handle).await;
156156+ }
157157+ }
158158+159159+ pub async fn create_lobby(&mut self, handle: StateHandle, settings: GameSettings) -> Result {
160160+ let sender = self.sender();
161161+162162+ let code = request_room_code()
163163+ .await
164164+ .context("Failed to get room code")?;
165165+166166+ let lobby = Lobby::new(&code, true, self.profile.clone(), settings, sender)
167167+ .await
168168+ .context("Failed to start lobby")?;
169169+170170+ self.change_screen(DaemonScreen::Lobby(lobby)).await;
171171+ self.lobby_loop(handle).await;
172172+173173+ Ok(())
174174+ }
175175+176176+ pub async fn join_lobby(&mut self, handle: StateHandle, code: &str) -> Result {
177177+ let sender = self.sender();
178178+ // TODO: Lobby should not require this on join, use an [Option]?
179179+ let settings = GameSettings::default();
180180+181181+ let lobby = Lobby::new(code, false, self.profile.clone(), settings, sender)
182182+ .await
183183+ .context("Failed to join lobby")?;
184184+185185+ self.change_screen(DaemonScreen::Lobby(lobby)).await;
186186+ self.lobby_loop(handle).await;
187187+188188+ Ok(())
189189+ }
190190+191191+ fn assert_screen(&self, expected: ScreenUpdate) -> Result<(), TestingResponse> {
192192+ if self.screen.as_update() == expected {
193193+ Ok(())
194194+ } else {
195195+ Err(TestingResponse::WrongScreen)
196196+ }
197197+ }
198198+199199+ async fn process_lobby_req(&mut self, req: LobbyRequest) {
200200+ if let DaemonScreen::Lobby(lobby) = &self.screen {
201201+ let lobby = lobby.clone();
202202+ match req {
203203+ LobbyRequest::SwitchTeams(seeker) => lobby.switch_teams(seeker).await,
204204+ LobbyRequest::HostStartGame => lobby.start_game().await,
205205+ LobbyRequest::HostUpdateSettings(game_settings) => {
206206+ lobby.update_settings(game_settings).await
207207+ }
208208+ LobbyRequest::Leave => lobby.quit_lobby().await,
209209+ }
210210+ }
211211+ }
212212+213213+ async fn process_game_req(&mut self, req: GameRequest) {
214214+ if let DaemonScreen::Game(game) = &self.screen {
215215+ let game = game.clone();
216216+ match req {
217217+ GameRequest::NextTick => tokio::time::sleep(Self::INTERVAL).await,
218218+ GameRequest::MarkCaught => game.mark_caught().await,
219219+ GameRequest::GetPowerup => game.get_powerup().await,
220220+ GameRequest::UsePowerup => game.use_powerup().await,
221221+ GameRequest::ForcePowerup(power_up_type) => {
222222+ let mut state = game.lock_state().await;
223223+ state.force_set_powerup(power_up_type);
224224+ }
225225+ GameRequest::Quit => game.quit_game().await,
226226+ }
227227+ }
228228+ }
229229+230230+ pub async fn process_req(
231231+ &mut self,
232232+ handle: StateHandle,
233233+ req: TestingRequest,
234234+ ) -> Result<(), TestingResponse> {
235235+ match req {
236236+ TestingRequest::StartLobby(game_settings) => {
237237+ self.assert_screen(ScreenUpdate::PreConnect)?;
238238+ self.create_lobby(handle, game_settings).await?;
239239+ }
240240+ TestingRequest::JoinLobby(code) => {
241241+ self.assert_screen(ScreenUpdate::PreConnect)?;
242242+ self.join_lobby(handle, &code).await?;
243243+ }
244244+ TestingRequest::LobbyReq(lobby_request) => {
245245+ self.assert_screen(ScreenUpdate::Lobby)?;
246246+ self.process_lobby_req(lobby_request).await;
247247+ }
248248+ TestingRequest::GameReq(game_request) => {
249249+ self.assert_screen(ScreenUpdate::Game)?;
250250+ self.process_game_req(game_request).await;
251251+ }
252252+ }
253253+ Ok(())
254254+ }
255255+}
256256+257257+use interprocess::local_socket::{ListenerOptions, tokio::prelude::*};
258258+259259+const CLI_MSG: &str = "Usage: manhunt-test-daemon SOCKET_NAME PLAYER_NAME";
260260+261261+#[tokio::main(flavor = "current_thread")]
262262+pub async fn main() -> Result {
263263+ let args = std::env::args().collect::<Vec<_>>();
264264+ let raw_socket_name = args.get(1).cloned().expect(CLI_MSG);
265265+ let player_name = args.get(2).cloned().expect(CLI_MSG);
266266+ let socket_name = get_socket_name(raw_socket_name)?;
267267+ let opts = ListenerOptions::new().name(socket_name);
268268+ let listener = opts.create_tokio().context("Failed to bind to socket")?;
269269+ let (resp_tx, mut resp_rx) = mpsc::channel::<TestingResponse>(40);
270270+271271+ let handle = Arc::new(Mutex::new(DaemonState::new(player_name, resp_tx)));
272272+273273+ eprintln!("Testing Daemon Ready");
274274+275275+ 'server: loop {
276276+ let res = tokio::select! {
277277+ res = listener.accept() => {
278278+ res
279279+ },
280280+ Ok(_) = tokio::signal::ctrl_c() => {
281281+ break 'server;
282282+ }
283283+ };
284284+285285+ match res {
286286+ Ok(stream) => {
287287+ let mut recv = BufReader::new(&stream);
288288+ let mut send = &stream;
289289+290290+ let mut buffer = String::with_capacity(256);
291291+292292+ loop {
293293+ tokio::select! {
294294+ Ok(_) = tokio::signal::ctrl_c() => {
295295+ break 'server;
296296+ }
297297+ res = recv.read_line(&mut buffer) => {
298298+ match res {
299299+ Ok(0) => {
300300+ break;
301301+ }
302302+ Ok(_amnt) => {
303303+ let req = serde_json::from_str(&buffer).expect("Failed to parse");
304304+ buffer.clear();
305305+ let handle2 = handle.clone();
306306+ let mut state = handle.lock().await;
307307+ if let Err(resp) = state.process_req(handle2, req).await {
308308+ let encoded = serde_json::to_vec(&resp).expect("Failed to encode");
309309+ send.write_all(&encoded).await.expect("Failed to send");
310310+ }
311311+ }
312312+ Err(why) => {
313313+ eprintln!("Read Error: {why:?}");
314314+ }
315315+ }
316316+ }
317317+ Some(resp) = resp_rx.recv() => {
318318+ let encoded = serde_json::to_vec(&resp).expect("Failed to encode");
319319+ send.write_all(&encoded).await.expect("Failed to send");
320320+ }
321321+ }
322322+ }
323323+ }
324324+ Err(why) => eprintln!("Error from connection: {why:?}"),
325325+ }
326326+ }
327327+328328+ Ok(())
329329+}
+99
manhunt-testing/src/driver.rs
···11+use clap::{Parser, Subcommand, ValueEnum};
22+use interprocess::local_socket::{tokio::Stream, traits::tokio::Stream as _};
33+use manhunt_logic::PowerUpType;
44+use manhunt_test_shared::{get_socket_name, prelude::*};
55+66+#[derive(Parser)]
77+struct Cli {
88+ /// Path to the UNIX domain socket the test daemon is listening on
99+ socket: String,
1010+1111+ #[command(subcommand)]
1212+ command: Commands,
1313+}
1414+1515+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
1616+enum Role {
1717+ Seeker,
1818+ Hider,
1919+}
2020+2121+#[derive(Subcommand)]
2222+enum LobbyCommand {
2323+ /// Switch teams between seekers and hiders
2424+ SwitchTeams {
2525+ /// The role you want to become
2626+ #[arg(value_enum)]
2727+ role: Role,
2828+ },
2929+ /// (Host) Sync game settings to players
3030+ SyncSettings,
3131+ /// (Host) Start the game for everyone
3232+ StartGame,
3333+ /// Quit to the main menu
3434+ Quit,
3535+}
3636+3737+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
3838+enum PowerUpTypeValue {
3939+ PingSeeker,
4040+ PingAllSeekers,
4141+ ForcePingOther,
4242+}
4343+4444+impl From<PowerUpTypeValue> for PowerUpType {
4545+ fn from(value: PowerUpTypeValue) -> Self {
4646+ match value {
4747+ PowerUpTypeValue::PingSeeker => PowerUpType::PingSeeker,
4848+ PowerUpTypeValue::PingAllSeekers => PowerUpType::PingAllSeekers,
4949+ PowerUpTypeValue::ForcePingOther => PowerUpType::ForcePingOther,
5050+ }
5151+ }
5252+}
5353+5454+#[derive(Subcommand)]
5555+enum GameCommand {
5656+ /// Mark the local player as caught for everyone
5757+ MarkCaught,
5858+ /// Get a currently available powerup
5959+ GetPowerup,
6060+ /// Use the held powerup of the local player
6161+ UsePowerup,
6262+ /// Force set the held powerup to the given type
6363+ ForcePowerup {
6464+ #[arg(value_enum)]
6565+ ptype: PowerUpTypeValue,
6666+ },
6767+ /// Quit the game
6868+ Quit,
6969+}
7070+7171+#[derive(Subcommand)]
7272+enum Commands {
7373+ /// Create a lobby
7474+ Create,
7575+ /// Join a lobby
7676+ Join {
7777+ /// The join code for the lobby
7878+ join_code: String,
7979+ },
8080+ /// Execute a command in an active lobby
8181+ #[command(subcommand)]
8282+ Lobby(LobbyCommand),
8383+ /// Execute a command in an active game
8484+ #[command(subcommand)]
8585+ Game(GameCommand),
8686+}
8787+8888+#[tokio::main]
8989+async fn main() -> Result {
9090+ let cli = Cli::parse();
9191+9292+ let socket_name = get_socket_name(cli.socket.clone()).context("Failed to get socket name")?;
9393+9494+ let _stream = Stream::connect(socket_name)
9595+ .await
9696+ .context("Failed to connect to socket")?;
9797+9898+ Ok(())
9999+}
···44edition = "2024"
5566[dependencies]
77-anyhow = "1.0.98"
88-futures = "0.3.31"
99-log = "0.4.27"
1010-matchbox_protocol = "0.12.0"
1111-matchbox_socket = "0.12.0"
1212-rmp-serde = "1.3.0"
1313-serde = { version = "1.0.219", features = ["derive"] }
1414-tokio = { version = "1.45.1", features = ["macros", "sync", "time", "rt"] }
1515-tokio-util = "0.7.15"
1616-uuid = { version = "1.17.0", features = ["serde"] }
77+anyhow = "1.0.101"
88+futures = "0.3.32"
99+log = "0.4.29"
1010+matchbox_protocol = "0.14.0"
1111+matchbox_socket = "0.14.0"
1212+rmp-serde = "1.3.1"
1313+serde = { version = "1.0.228", features = ["derive"] }
1414+tokio = { version = "1.49.0", features = ["macros", "sync", "time", "rt"] }
1515+tokio-util = "0.7.18"
1616+uuid = { version = "1.21.0", features = ["serde"] }
1717manhunt-logic = { version = "0.1.0", path = "../manhunt-logic" }
1818-rand = { version = "0.9.1", features = ["thread_rng"] }
1919-reqwest = { version = "0.12.20", default-features = false, features = ["charset", "http2", "rustls-tls", "system-proxy"] }
2020-const-str = "0.6.2"
1818+rand = { version = "0.10.0", features = ["thread_rng"] }
1919+reqwest = { version = "0.13.2", default-features = false, features = ["charset", "http2", "default-tls", "system-proxy"] }
2020+const-str = "1.1.0"
+1-1
manhunt-transport/src/lib.rs
···33mod server;
4455pub use matchbox::MatchboxTransport;
66-pub use server::{generate_join_code, room_exists};
66+pub use server::{request_room_code, room_exists};
+731-120
manhunt-transport/src/matchbox.rs
···11-use std::{pin::Pin, sync::Arc, time::Duration};
11+use std::{collections::HashSet, marker::PhantomData, pin::Pin, sync::Arc};
2233use anyhow::{Context, anyhow};
44-use futures::FutureExt;
55-use log::error;
44+use futures::{
55+ SinkExt, Stream, StreamExt,
66+ channel::mpsc::{UnboundedReceiver, UnboundedSender},
77+};
88+use log::{error, info};
69use matchbox_socket::{Error as SocketError, PeerId, PeerState, WebRtcSocket};
710use tokio::{
811 sync::{Mutex, mpsc},
···1922type MsgPair = (Option<Uuid>, TransportMessage);
2023type Queue = QueuePair<MsgPair>;
21242222-pub struct MatchboxTransport {
2525+pub struct MatchboxTransport<S: SocketImpl = WebRtcSocket> {
2326 my_id: Uuid,
2427 incoming: Queue,
2525- outgoing: Queue,
2828+ all_peers: Mutex<HashSet<Uuid>>,
2929+ msg_sender: UnboundedSender<MatchboxMsgPair>,
2630 cancel_token: CancellationToken,
3131+ phantom: PhantomData<S>,
2732}
28332934type LoopFutRes = Result<(), SocketError>;
30353636+type MatchboxMsgPair = (PeerId, Box<[u8]>);
3737+type MatchboxSender = UnboundedSender<MatchboxMsgPair>;
3838+type MatchboxReceiver = UnboundedReceiver<MatchboxMsgPair>;
3939+type MatchboxChannel = (MatchboxSender, MatchboxReceiver);
4040+3141fn map_socket_error(err: SocketError) -> anyhow::Error {
3242 match err {
3343 SocketError::ConnectionFailed(e) => anyhow!("Connection to server failed: {e:?}"),
···3545 }
3646}
37473838-impl MatchboxTransport {
3939- pub async fn new(join_code: &str, is_host: bool) -> Result<Arc<Self>> {
4040- let (itx, irx) = mpsc::channel(15);
4141- let (otx, orx) = mpsc::channel(15);
4848+type FutPin<T> = Pin<Box<dyn Future<Output = T> + Send>>;
4949+type MessageLoopFuture = FutPin<LoopFutRes>;
5050+type PeerMsg = (PeerId, PeerState);
42515252+pub trait SocketImpl: Unpin + Send + Sync + Sized + Stream<Item = PeerMsg> {
5353+ fn new(room_url: &str) -> (Self, MessageLoopFuture);
5454+ fn get_id(&mut self) -> Option<PeerId>;
5555+ fn take_channel(&mut self) -> MatchboxChannel;
5656+}
5757+5858+impl SocketImpl for WebRtcSocket {
5959+ fn new(room_url: &str) -> (Self, MessageLoopFuture) {
6060+ Self::new_reliable(room_url)
6161+ }
6262+6363+ fn get_id(&mut self) -> Option<PeerId> {
6464+ self.id()
6565+ }
6666+6767+ fn take_channel(&mut self) -> MatchboxChannel {
6868+ self.take_channel(0).expect("Failed to get channel").split()
6969+ }
7070+}
7171+7272+impl<S: SocketImpl + 'static> MatchboxTransport<S> {
7373+ pub async fn new(join_code: &str, is_host: bool) -> Result<Arc<Self>> {
4374 let ws_url = server::room_url(join_code, is_host);
7575+ let (socket, loop_fut) = S::new(&ws_url);
7676+ Self::from_socket_and_loop_fut(socket, loop_fut).await
7777+ }
44784545- let (mut socket, mut loop_fut) = WebRtcSocket::new_reliable(&ws_url);
7979+ async fn from_socket_and_loop_fut(
8080+ mut socket: S,
8181+ mut loop_fut: MessageLoopFuture,
8282+ ) -> Result<Arc<Self>> {
8383+ let (itx, irx) = mpsc::channel(15);
8484+ let (mtx, mrx) = socket.take_channel();
46854786 let res = loop {
4887 tokio::select! {
4988 id = Self::wait_for_id(&mut socket) => {
5089 if let Some(id) = id {
5151- break Ok(id);
9090+ break Ok(id);
5291 }
5392 },
5493 res = &mut loop_fut => {
···66105 let transport = Arc::new(Self {
67106 my_id,
68107 incoming: (itx, Mutex::new(irx)),
6969- outgoing: (otx, Mutex::new(orx)),
108108+ all_peers: Mutex::new(HashSet::with_capacity(5)),
109109+ msg_sender: mtx.clone(),
70110 cancel_token: CancellationToken::new(),
111111+ phantom: PhantomData,
71112 });
7211373114 tokio::spawn({
74115 let transport = transport.clone();
75116 async move {
7676- transport.main_loop(socket, loop_fut).await;
117117+ transport.main_loop(socket, loop_fut, mrx).await;
77118 }
78119 });
7912080121 Ok(transport)
81122 }
82123 Err(why) => {
124124+ drop(mrx);
125125+ drop(mtx);
83126 drop(socket);
8484- loop_fut.await.context("While disconnecting")?;
85127 Err(why)
86128 }
87129 }
88130 }
891319090- async fn wait_for_id(socket: &mut WebRtcSocket) -> Option<Uuid> {
9191- if let Some(id) = socket.id() {
132132+ async fn wait_for_id(socket: &mut S) -> Option<Uuid> {
133133+ if let Some(id) = socket.get_id() {
92134 Some(id.0)
93135 } else {
94136 yield_now().await;
···104146 .expect("Failed to push to incoming queue");
105147 }
106148107107- async fn push_many_incoming(&self, msgs: Vec<MsgPair>) {
108108- let senders = self
109109- .incoming
110110- .0
111111- .reserve_many(msgs.len())
112112- .await
113113- .expect("Failed to reserve in incoming queue");
114114-115115- for (sender, msg) in senders.into_iter().zip(msgs.into_iter()) {
116116- sender.send(msg);
117117- }
118118- }
119119-120149 async fn main_loop(
121150 &self,
122122- mut socket: WebRtcSocket,
123123- loop_fut: Pin<Box<dyn Future<Output = LoopFutRes> + Send + 'static>>,
151151+ mut socket: S,
152152+ loop_fut: MessageLoopFuture,
153153+ mut mrx: UnboundedReceiver<MatchboxMsgPair>,
124154 ) {
125125- let loop_fut = async {
126126- let msg = match loop_fut.await {
127127- Ok(_) => TransportMessage::Disconnected,
128128- Err(e) => {
129129- let msg = map_socket_error(e).to_string();
130130- TransportMessage::Error(msg)
131131- }
132132- };
133133- self.push_incoming(None, msg).await;
134134- }
135135- .fuse();
136136-137155 tokio::pin!(loop_fut);
138138-139139- let mut interval = tokio::time::interval(Duration::from_secs(1));
140140-141141- let mut outgoing_rx = self.outgoing.1.lock().await;
142142- const MAX_MSG_SEND: usize = 30;
143143- let mut message_buffer = Vec::with_capacity(MAX_MSG_SEND);
144144-145156 let mut packet_handler = PacketHandler::default();
146157147147- loop {
148148- self.handle_peers(&mut socket).await;
149149-150150- self.handle_recv(&mut socket, &mut packet_handler).await;
158158+ info!("Starting transport loop");
151159160160+ let (should_await, msg) = loop {
152161 tokio::select! {
153162 biased;
154163164164+ res = &mut loop_fut => {
165165+ info!("Transport-initiated disconnect");
166166+ break (false, match res {
167167+ Ok(_) => TransportMessage::Disconnected,
168168+ Err(e) => {
169169+ let msg = map_socket_error(e).to_string();
170170+ TransportMessage::Error(msg)
171171+ }
172172+ });
173173+ }
174174+155175 _ = self.cancel_token.cancelled() => {
156156- break;
176176+ info!("Logic-initiated disconnect");
177177+ break (true, TransportMessage::Disconnected);
157178 }
158179159159- _ = &mut loop_fut => {
160160- break;
180180+ Some((peer, state)) = socket.next() => {
181181+ info!("Handling peer {peer}: {state:?}");
182182+ self.handle_peer(peer, state).await;
161183 }
162184163163- _ = outgoing_rx.recv_many(&mut message_buffer, MAX_MSG_SEND) => {
164164- let peers = socket.connected_peers().collect::<Vec<_>>();
165165- self.handle_send(&mut socket, &peers, &mut message_buffer).await;
185185+ Some(data) = mrx.next() => {
186186+ info!("Handling new packet from {}", data.0);
187187+ self.handle_recv(data, &mut packet_handler).await;
166188 }
167189168168- _ = interval.tick() => {
169169- continue;
170170- }
190190+191191+ }
192192+ };
193193+194194+ self.push_incoming(Some(self.my_id), msg).await;
195195+196196+ self.msg_sender.close_channel();
197197+ self.incoming.1.lock().await.close();
198198+ drop(mrx);
199199+ drop(socket);
200200+ if should_await {
201201+ if let Err(why) = loop_fut.await {
202202+ error!("Failed to await after disconnect: {why:?}");
171203 }
172204 }
205205+ info!("Transport disconnected");
173206 }
174207175175- async fn handle_peers(&self, socket: &mut WebRtcSocket) {
176176- for (peer, state) in socket.update_peers() {
177177- let msg = match state {
178178- PeerState::Connected => TransportMessage::PeerConnect(peer.0),
179179- PeerState::Disconnected => TransportMessage::PeerDisconnect(peer.0),
180180- };
181181- self.push_incoming(Some(peer.0), msg).await;
182182- }
208208+ async fn handle_peer(&self, peer: PeerId, state: PeerState) {
209209+ let mut all_peers = self.all_peers.lock().await;
210210+ let msg = match state {
211211+ PeerState::Connected => {
212212+ all_peers.insert(peer.0);
213213+ TransportMessage::PeerConnect(peer.0)
214214+ }
215215+ PeerState::Disconnected => {
216216+ all_peers.remove(&peer.0);
217217+ TransportMessage::PeerDisconnect(peer.0)
218218+ }
219219+ };
220220+ drop(all_peers);
221221+ self.push_incoming(Some(peer.0), msg).await;
183222 }
184223185185- async fn handle_send(
224224+ async fn handle_recv(
186225 &self,
187187- socket: &mut WebRtcSocket,
188188- all_peers: &[PeerId],
189189- messages: &mut Vec<MsgPair>,
226226+ (PeerId(peer), packet): MatchboxMsgPair,
227227+ handler: &mut PacketHandler,
190228 ) {
191191- let encoded_messages = messages.drain(..).filter_map(|(id, msg)| {
192192- match PacketHandler::message_to_packets(&msg) {
193193- Ok(packets) => Some((id, packets)),
194194- Err(why) => {
195195- error!("Error encoding message to packets: {why:?}");
196196- None
197197- }
229229+ match handler.consume_packet(peer, packet.into_vec()) {
230230+ Ok(Some(msg)) => {
231231+ self.push_incoming(Some(peer), msg).await;
198232 }
199199- });
233233+ Ok(None) => {
234234+ // Non complete message
235235+ }
236236+ Err(why) => {
237237+ error!("Error receiving message: {why}");
238238+ }
239239+ }
240240+ }
200241201201- let channel = socket.channel_mut(0);
242242+ pub async fn send_transport_message(&self, peer: Option<Uuid>, msg: TransportMessage) {
243243+ let mut tx = self.msg_sender.clone();
202244203203- for (peer, packets) in encoded_messages {
204204- if let Some(peer) = peer {
205205- for packet in packets {
206206- channel.send(packet.into_boxed_slice(), PeerId(peer));
207207- }
208208- } else {
209209- for packet in packets {
210210- let boxed = packet.into_boxed_slice();
211211- for peer in all_peers {
212212- channel.send(boxed.clone(), *peer);
245245+ match PacketHandler::message_to_packets(&msg) {
246246+ Ok(packets) => {
247247+ if let Some(peer) = peer {
248248+ let mut stream = futures::stream::iter(
249249+ packets
250250+ .into_iter()
251251+ .map(|p| Ok((PeerId(peer), p.into_boxed_slice()))),
252252+ );
253253+ if let Err(why) = tx.send_all(&mut stream).await {
254254+ error!("Error sending packet: {why}");
255255+ }
256256+ } else {
257257+ let all_peers = self.all_peers.lock().await;
258258+ for peer in all_peers.iter().copied() {
259259+ let packets = packets.clone();
260260+ let mut stream = futures::stream::iter(
261261+ packets
262262+ .into_iter()
263263+ .map(|p| Ok((PeerId(peer), p.into_boxed_slice()))),
264264+ );
265265+ if let Err(why) = tx.send_all(&mut stream).await {
266266+ error!("Error sending packet: {why}");
267267+ }
213268 }
214269 }
215270 }
271271+ Err(why) => {
272272+ error!("Error encoding message: {why}");
273273+ }
216274 }
217275 }
218276219219- async fn handle_recv(&self, socket: &mut WebRtcSocket, handler: &mut PacketHandler) {
220220- let data = socket.channel_mut(0).receive();
221221- let messages = data
222222- .into_iter()
223223- .filter_map(
224224- |(peer, bytes)| match handler.consume_packet(peer.0, bytes.into_vec()) {
225225- Ok(msg) => msg.map(|msg| (Some(peer.0), msg)),
226226- Err(why) => {
227227- error!("Error receiving message: {why}");
228228- None
229229- }
230230- },
231231- )
232232- .collect();
233233- self.push_many_incoming(messages).await;
277277+ pub async fn recv_transport_messages(&self) -> Vec<MsgPair> {
278278+ let mut incoming_rx = self.incoming.1.lock().await;
279279+ let mut buffer = Vec::with_capacity(60);
280280+ incoming_rx.recv_many(&mut buffer, 60).await;
281281+ buffer
234282 }
235283236236- pub async fn send_transport_message(&self, peer: Option<Uuid>, msg: TransportMessage) {
237237- self.outgoing
238238- .0
239239- .send((peer, msg))
284284+ #[cfg(test)]
285285+ pub async fn force_recv_msg(&self) -> MsgPair {
286286+ self.incoming
287287+ .1
288288+ .lock()
289289+ .await
290290+ .recv()
240291 .await
241241- .expect("Failed to add to outgoing queue");
292292+ .expect("No messages")
242293 }
243294244244- pub async fn recv_transport_messages(&self) -> Vec<MsgPair> {
245245- let mut incoming_rx = self.incoming.1.lock().await;
246246- let mut buffer = Vec::with_capacity(60);
247247- incoming_rx.recv_many(&mut buffer, 60).await;
248248- buffer
295295+ #[cfg(test)]
296296+ pub async fn assert_no_incoming(&self) {
297297+ assert!(self.incoming.1.lock().await.is_empty());
249298 }
250299251300 pub fn cancel(&self) {
···253302 }
254303}
255304256256-impl Transport for MatchboxTransport {
305305+impl<S: SocketImpl + 'static> Transport for MatchboxTransport<S> {
257306 fn self_id(&self) -> Uuid {
258307 self.my_id
259308 }
···292341 Self::new(code, host).await
293342 }
294343}
344344+345345+#[cfg(test)]
346346+mod tests {
347347+348348+ use futures::{
349349+ channel::{mpsc, oneshot},
350350+ lock::Mutex as FutMutex,
351351+ };
352352+ use manhunt_logic::{GameEvent, LobbyMessage, PlayerProfile};
353353+ use matchbox_socket::SignalingError;
354354+355355+ use super::*;
356356+ use tokio::test;
357357+358358+ use std::{collections::HashMap, sync::Mutex as StdMutex, time::Duration};
359359+360360+ type PeerRx = UnboundedReceiver<PeerMsg>;
361361+ type PeerTx = UnboundedSender<PeerMsg>;
362362+ type IdHandle = Arc<StdMutex<Option<PeerId>>>;
363363+364364+ struct MockSocket {
365365+ peer_recv: PeerRx,
366366+ id: IdHandle,
367367+ channel: Option<MatchboxChannel>,
368368+ cancel: CancellationToken,
369369+ }
370370+371371+ impl MockSocket {
372372+ pub fn new(
373373+ peer_recv: PeerRx,
374374+ channel: MatchboxChannel,
375375+ id: IdHandle,
376376+ ) -> (
377377+ Self,
378378+ MessageLoopFuture,
379379+ oneshot::Sender<LoopFutRes>,
380380+ CancellationToken,
381381+ ) {
382382+ let (stop_tx, stop_rx) = oneshot::channel();
383383+ let cancel = CancellationToken::new();
384384+ let sock = Self {
385385+ peer_recv,
386386+ channel: Some(channel),
387387+ id,
388388+ cancel: cancel.clone(),
389389+ };
390390+391391+ let fut = Box::pin(async move { stop_rx.await.expect("Failed to recv") });
392392+393393+ (sock, fut, stop_tx, cancel)
394394+ }
395395+ }
396396+397397+ impl Drop for MockSocket {
398398+ fn drop(&mut self) {
399399+ self.cancel.cancel();
400400+ }
401401+ }
402402+403403+ impl Stream for MockSocket {
404404+ type Item = (PeerId, PeerState);
405405+406406+ fn poll_next(
407407+ self: Pin<&mut Self>,
408408+ cx: &mut std::task::Context<'_>,
409409+ ) -> std::task::Poll<Option<Self::Item>> {
410410+ let mut peer_state_rx = Pin::new(&mut self.get_mut().peer_recv);
411411+ peer_state_rx.as_mut().poll_next(cx)
412412+ }
413413+ }
414414+415415+ impl SocketImpl for MockSocket {
416416+ fn new(_room_url: &str) -> (Self, MessageLoopFuture) {
417417+ unreachable!("Tests should use [MatchboxTransport::from_socket_and_loop_fut]")
418418+ }
419419+420420+ fn get_id(&mut self) -> Option<PeerId> {
421421+ *self.id.lock().unwrap()
422422+ }
423423+424424+ fn take_channel(&mut self) -> (MatchboxSender, MatchboxReceiver) {
425425+ self.channel.take().expect("Channel already taken")
426426+ }
427427+ }
428428+429429+ type MatchboxTransport = super::MatchboxTransport<MockSocket>;
430430+431431+ struct WaitingPeer {
432432+ incoming: MatchboxSender,
433433+ outgoing: MatchboxReceiver,
434434+ peer_tx: PeerTx,
435435+ intended_id: PeerId,
436436+ id_handle: IdHandle,
437437+ disconnect: oneshot::Sender<LoopFutRes>,
438438+ client_cancel: CancellationToken,
439439+ }
440440+441441+ #[derive(Default, Debug)]
442442+ struct MockSignaling {
443443+ peers: HashMap<
444444+ PeerId,
445445+ (
446446+ PeerTx,
447447+ oneshot::Sender<LoopFutRes>,
448448+ CancellationToken,
449449+ CancellationToken,
450450+ ),
451451+ >,
452452+ senders: Arc<FutMutex<HashMap<PeerId, MatchboxSender>>>,
453453+ }
454454+455455+ impl MockSignaling {
456456+ fn new() -> Self {
457457+ tokio::time::pause();
458458+ Self::default()
459459+ }
460460+461461+ fn client_connect(
462462+ &self,
463463+ id: Uuid,
464464+ ) -> (
465465+ WaitingPeer,
466466+ FutPin<Result<Arc<MatchboxTransport>, anyhow::Error>>,
467467+ ) {
468468+ let (itx, irx) = mpsc::unbounded();
469469+ let (otx, orx) = mpsc::unbounded();
470470+ let (peer_tx, peer_rx) = mpsc::unbounded();
471471+ let id_handle = Arc::new(StdMutex::new(None));
472472+473473+ let (sock, fut, disconnect, cancel) =
474474+ MockSocket::new(peer_rx, (otx, irx), id_handle.clone());
475475+476476+ let transport_fut = Box::pin(MatchboxTransport::from_socket_and_loop_fut(sock, fut));
477477+478478+ let peer = WaitingPeer {
479479+ incoming: itx,
480480+ outgoing: orx,
481481+ peer_tx,
482482+ intended_id: PeerId(id),
483483+ id_handle,
484484+ disconnect,
485485+ client_cancel: cancel,
486486+ };
487487+488488+ (peer, transport_fut)
489489+ }
490490+491491+ async fn broadcast_peer_join(&mut self, source: PeerId) {
492492+ let (source_sender, _, _, _) = self.peers.get(&source).expect("Source not in peers");
493493+ let mut source_sender = source_sender.clone();
494494+ let peers = self.peers.iter_mut().filter(|(k, _)| **k != source);
495495+ for (id, (peer_tx, _, _, _)) in peers {
496496+ peer_tx
497497+ .send((source, PeerState::Connected))
498498+ .await
499499+ .expect("Failed to send");
500500+ source_sender
501501+ .send((*id, PeerState::Connected))
502502+ .await
503503+ .expect("Failed to send");
504504+ }
505505+ }
506506+507507+ async fn broadcast_peer_leave(&mut self, source: PeerId) {
508508+ let peers = self.peers.iter_mut().filter(|(k, _)| **k != source);
509509+ for (_, (peer_tx, _, _, _)) in peers {
510510+ peer_tx
511511+ .send((source, PeerState::Disconnected))
512512+ .await
513513+ .expect("Failed to send");
514514+ }
515515+ }
516516+517517+ /// Assign an ID to a MockSocket and set it so the future resolves
518518+ async fn assign_id(&mut self, waiting: WaitingPeer) {
519519+ let WaitingPeer {
520520+ id_handle,
521521+ intended_id,
522522+ incoming,
523523+ mut outgoing,
524524+ peer_tx,
525525+ disconnect,
526526+ client_cancel,
527527+ } = waiting;
528528+529529+ let cancel = CancellationToken::new();
530530+531531+ *id_handle.lock().unwrap() = Some(intended_id);
532532+ self.peers.insert(
533533+ intended_id,
534534+ (peer_tx, disconnect, cancel.clone(), client_cancel),
535535+ );
536536+ self.senders.lock().await.insert(intended_id, incoming);
537537+ self.broadcast_peer_join(intended_id).await;
538538+539539+ let senders = self.senders.clone();
540540+541541+ tokio::spawn(async move {
542542+ let id = intended_id;
543543+ loop {
544544+ tokio::select! {
545545+ biased;
546546+547547+ _ = cancel.cancelled() => { break; }
548548+549549+ Some((peer, packet)) = outgoing.next() => {
550550+ let mut senders = senders.lock().await;
551551+ let sender = senders.get_mut(&peer).expect("Failed to find peer");
552552+ sender.send((id, packet)).await.expect("Failed to send");
553553+ }
554554+ }
555555+ }
556556+ });
557557+ }
558558+559559+ async fn disconnect_peer(&mut self, id: Uuid, res: LoopFutRes) {
560560+ let (_, dc, cancel, _) = self.peers.remove(&PeerId(id)).expect("Peer not connected");
561561+ cancel.cancel();
562562+ dc.send(res).expect("Failed to send dc");
563563+ self.broadcast_peer_leave(PeerId(id)).await;
564564+ }
565565+566566+ async fn wait_for_socket_drop(&self, id: Uuid) {
567567+ let cancel = self.peers.get(&PeerId(id)).unwrap().3.clone();
568568+ cancel.cancelled().await;
569569+ }
570570+571571+ async fn wait_for_client_disconnected(&mut self, id: Uuid) {
572572+ self.wait_for_socket_drop(id).await;
573573+ self.disconnect_peer(id, Ok(())).await;
574574+ }
575575+576576+ async fn wait(&self) {
577577+ tokio::time::sleep(Duration::from_millis(1)).await;
578578+ }
579579+580580+ async fn quick_join(&mut self, id: Uuid) -> Arc<MatchboxTransport> {
581581+ let (wait, fut) = self.client_connect(id);
582582+ self.assign_id(wait).await;
583583+ fut.await.expect("Transport init failed")
584584+ }
585585+ }
586586+587587+ const fn id(x: u128) -> Uuid {
588588+ Uuid::from_u128(x)
589589+ }
590590+591591+ #[test]
592592+ async fn test_full_loop() {
593593+ let mut sig = MockSignaling::new();
594594+595595+ let (wait, fut) = sig.client_connect(id(1));
596596+597597+ sig.assign_id(wait).await;
598598+599599+ let transport = fut.await.expect("Tansport failed to initialize");
600600+601601+ assert_eq!(transport.my_id, id(1));
602602+603603+ transport.disconnect().await;
604604+605605+ sig.wait_for_client_disconnected(id(1)).await;
606606+ }
607607+608608+ #[test]
609609+ async fn test_dc_pre_assign() {
610610+ let sig = MockSignaling::new();
611611+612612+ let (wait, fut) = sig.client_connect(id(1));
613613+614614+ wait.disconnect.send(Ok(())).expect("Failed to send");
615615+616616+ let res = fut.await;
617617+618618+ assert!(res.is_err());
619619+ assert!(wait.incoming.is_closed());
620620+ assert!(wait.peer_tx.is_closed());
621621+ assert!(wait.client_cancel.is_cancelled());
622622+ }
623623+624624+ #[test]
625625+ async fn test_err_pre_assign() {
626626+ let sig = MockSignaling::new();
627627+628628+ let (wait, fut) = sig.client_connect(id(1));
629629+630630+ wait.disconnect
631631+ .send(Err(SocketError::Disconnected(
632632+ SignalingError::UnknownFormat,
633633+ )))
634634+ .expect("Failed to send");
635635+636636+ let res = fut.await;
637637+638638+ assert!(res.is_err());
639639+ assert!(wait.incoming.is_closed());
640640+ assert!(wait.peer_tx.is_closed());
641641+ assert!(wait.client_cancel.is_cancelled());
642642+ }
643643+644644+ #[test]
645645+ async fn test_graceful_disconnect() {
646646+ let mut sig = MockSignaling::new();
647647+648648+ let (wait, fut) = sig.client_connect(id(1));
649649+650650+ let can = wait.client_cancel.clone();
651651+652652+ sig.assign_id(wait).await;
653653+654654+ let transport = fut.await.expect("Transport init failed");
655655+656656+ sig.disconnect_peer(id(1), Ok(())).await;
657657+658658+ let (_, disconnected) = transport
659659+ .incoming
660660+ .1
661661+ .lock()
662662+ .await
663663+ .recv()
664664+ .await
665665+ .expect("Transport didnt send error");
666666+667667+ assert!(matches!(disconnected, TransportMessage::Disconnected));
668668+669669+ can.cancelled().await;
670670+671671+ assert!(transport.incoming.0.is_closed());
672672+ assert!(transport.msg_sender.is_closed());
673673+ }
674674+675675+ #[test]
676676+ async fn test_error_handle() {
677677+ let mut sig = MockSignaling::new();
678678+679679+ let (wait, fut) = sig.client_connect(id(1));
680680+681681+ let can = wait.client_cancel.clone();
682682+683683+ sig.assign_id(wait).await;
684684+685685+ let transport = fut.await.expect("Transport init failed");
686686+687687+ sig.disconnect_peer(
688688+ id(1),
689689+ Err(SocketError::Disconnected(SignalingError::UnknownFormat)),
690690+ )
691691+ .await;
692692+693693+ let (_, disconnected) = transport
694694+ .incoming
695695+ .1
696696+ .lock()
697697+ .await
698698+ .recv()
699699+ .await
700700+ .expect("Transport didnt send error");
701701+702702+ assert!(matches!(disconnected, TransportMessage::Error(_)));
703703+704704+ // Wait for the transport to drop the socket
705705+ can.cancelled().await;
706706+707707+ assert!(transport.incoming.0.is_closed());
708708+ assert!(transport.msg_sender.is_closed());
709709+ }
710710+711711+ #[test]
712712+ async fn test_message_passing() {
713713+ let mut sig = MockSignaling::new();
714714+715715+ let t1 = sig.quick_join(id(1)).await;
716716+ let t2 = sig.quick_join(id(2)).await;
717717+718718+ sig.wait().await;
719719+720720+ let (_, msg) = t1.force_recv_msg().await;
721721+ let (_, msg2) = t2.force_recv_msg().await;
722722+723723+ assert_eq!(t1.all_peers.lock().await.len(), 1);
724724+ assert_eq!(t2.all_peers.lock().await.len(), 1);
725725+ assert!(matches!(msg, TransportMessage::PeerConnect(pid) if pid == id(2)));
726726+ assert!(matches!(msg2, TransportMessage::PeerConnect(pid) if pid == id(1)));
727727+728728+ t1.send_transport_message(Some(id(2)), GameEvent::PlayerCaught(id(1)).into())
729729+ .await;
730730+731731+ sig.wait().await;
732732+733733+ let (_, msg) = t2.force_recv_msg().await;
734734+735735+ assert!(
736736+ matches!(msg, TransportMessage::Game(ge) if matches!(*ge, GameEvent::PlayerCaught(i) if i == id(1)))
737737+ );
738738+739739+ t2.send_transport_message(None, LobbyMessage::PlayerSwitch(id(2), true).into())
740740+ .await;
741741+742742+ sig.wait().await;
743743+744744+ let (_, msg) = t1.force_recv_msg().await;
745745+746746+ assert!(
747747+ matches!(msg, TransportMessage::Lobby(lm) if matches!(*lm, LobbyMessage::PlayerSwitch(i, b) if i == id(2) && b))
748748+ );
749749+ }
750750+751751+ #[test]
752752+ async fn test_msg_broadcast() {
753753+ let mut sig = MockSignaling::new();
754754+755755+ let t1 = sig.quick_join(id(1)).await;
756756+ let t2 = sig.quick_join(id(2)).await;
757757+ let t3 = sig.quick_join(id(3)).await;
758758+ let t4 = sig.quick_join(id(4)).await;
759759+760760+ sig.wait().await;
761761+762762+ let ts = [t1, t2, t3, t4];
763763+764764+ for t in ts.iter() {
765765+ assert_eq!(t.all_peers.lock().await.len(), ts.len() - 1);
766766+ // Eat the PeerConnected messages
767767+ for _ in 0..(ts.len() - 1) {
768768+ t.force_recv_msg().await;
769769+ }
770770+ }
771771+772772+ ts[0]
773773+ .send_transport_message(None, GameEvent::PlayerCaught(id(1)).into())
774774+ .await;
775775+776776+ sig.wait().await;
777777+778778+ ts[0].assert_no_incoming().await;
779779+780780+ for t in ts.iter().skip(1) {
781781+ let (pid, msg) = t.force_recv_msg().await;
782782+ assert_eq!(pid, Some(id(1)));
783783+ assert!(
784784+ matches!(msg, TransportMessage::Game(ge) if matches!(*ge, GameEvent::PlayerCaught(i) if i == id(1)))
785785+ );
786786+ }
787787+ }
788788+789789+ #[test]
790790+ async fn test_direct_msg() {
791791+ let mut sig = MockSignaling::new();
792792+793793+ let t1 = sig.quick_join(id(1)).await;
794794+ let t2 = sig.quick_join(id(2)).await;
795795+ let t3 = sig.quick_join(id(3)).await;
796796+797797+ sig.wait().await;
798798+799799+ let ts = [t1, t2, t3];
800800+801801+ for t in ts.iter() {
802802+ assert_eq!(t.all_peers.lock().await.len(), ts.len() - 1);
803803+ // Eat the PeerConnected messages
804804+ for _ in 0..(ts.len() - 1) {
805805+ t.force_recv_msg().await;
806806+ }
807807+ }
808808+809809+ ts[0]
810810+ .send_transport_message(Some(id(2)), GameEvent::PlayerCaught(id(1)).into())
811811+ .await;
812812+813813+ sig.wait().await;
814814+815815+ ts[0].assert_no_incoming().await;
816816+ ts[2].assert_no_incoming().await;
817817+818818+ let (pid, msg) = ts[1].force_recv_msg().await;
819819+ assert_eq!(pid, Some(id(1)));
820820+ assert!(
821821+ matches!(msg, TransportMessage::Game(ge) if matches!(*ge, GameEvent::PlayerCaught(i) if i == id(1)))
822822+ );
823823+ }
824824+825825+ #[test]
826826+ async fn test_multiple_disconnect() {
827827+ let mut sig = MockSignaling::new();
828828+829829+ let t1 = sig.quick_join(id(1)).await;
830830+ let t2 = sig.quick_join(id(2)).await;
831831+ let t3 = sig.quick_join(id(3)).await;
832832+833833+ sig.wait().await;
834834+835835+ let ts = [t1, t2, t3];
836836+837837+ for t in ts.iter() {
838838+ assert_eq!(t.all_peers.lock().await.len(), ts.len() - 1);
839839+ // Eat the PeerConnected messages
840840+ for _ in 0..(ts.len() - 1) {
841841+ t.force_recv_msg().await;
842842+ }
843843+ }
844844+845845+ ts[0].disconnect().await;
846846+847847+ sig.wait_for_client_disconnected(id(1)).await;
848848+849849+ sig.wait().await;
850850+851851+ for t in ts.iter().skip(1) {
852852+ let (_, msg) = t.force_recv_msg().await;
853853+854854+ let all = t.all_peers.lock().await;
855855+ assert!(!all.contains(&id(1)));
856856+ assert!(matches!(msg, TransportMessage::PeerDisconnect(i) if i == id(1)));
857857+ }
858858+ }
859859+860860+ #[test]
861861+ async fn test_big_message() {
862862+ // Just a random string that's bigger than the max packet size
863863+ let pfp = "vbnsj".repeat(65560);
864864+ let pfp2 = pfp.clone();
865865+866866+ let mut sig = MockSignaling::new();
867867+868868+ let t1 = sig.quick_join(id(1)).await;
869869+ let t2 = sig.quick_join(id(2)).await;
870870+871871+ sig.wait().await;
872872+873873+ t1.force_recv_msg().await;
874874+ t2.force_recv_msg().await;
875875+876876+ t1.send_transport_message(
877877+ Some(id(2)),
878878+ LobbyMessage::PlayerSync(
879879+ id(1),
880880+ PlayerProfile {
881881+ display_name: "asdf".to_string(),
882882+ pfp_base64: Some(pfp2),
883883+ },
884884+ )
885885+ .into(),
886886+ )
887887+ .await;
888888+889889+ sig.wait().await;
890890+891891+ let (_, msg) = t2.force_recv_msg().await;
892892+893893+ if let TransportMessage::Lobby(le) = msg {
894894+ if let LobbyMessage::PlayerSync(i, p) = *le {
895895+ assert_eq!(i, id(1));
896896+ assert_eq!(p.display_name, "asdf".to_string());
897897+ assert_eq!(p.pfp_base64, Some(pfp));
898898+ } else {
899899+ panic!("Incorrect lobby message");
900900+ }
901901+ } else {
902902+ panic!("Incorrect message");
903903+ }
904904+ }
905905+}