Live location tracking and playback for the game "manhunt"

Transport improvements, add event for screen change

bwc9876.dev 769d1dca 91a1aeff

verified
+693 -128
+207 -4
Cargo.lock
··· 429 429 "futures-io", 430 430 "rustls 0.21.12", 431 431 "rustls-pemfile", 432 - "webpki-roots", 432 + "webpki-roots 0.22.6", 433 433 ] 434 434 435 435 [[package]] ··· 1022 1022 1023 1023 [[package]] 1024 1024 name = "core-foundation" 1025 + version = "0.9.4" 1026 + source = "registry+https://github.com/rust-lang/crates.io-index" 1027 + checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" 1028 + dependencies = [ 1029 + "core-foundation-sys", 1030 + "libc", 1031 + ] 1032 + 1033 + [[package]] 1034 + name = "core-foundation" 1025 1035 version = "0.10.1" 1026 1036 source = "registry+https://github.com/rust-lang/crates.io-index" 1027 1037 checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" ··· 1043 1053 checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" 1044 1054 dependencies = [ 1045 1055 "bitflags 2.9.1", 1046 - "core-foundation", 1056 + "core-foundation 0.10.1", 1047 1057 "core-graphics-types", 1048 1058 "foreign-types", 1049 1059 "libc", ··· 1056 1066 checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" 1057 1067 dependencies = [ 1058 1068 "bitflags 2.9.1", 1059 - "core-foundation", 1069 + "core-foundation 0.10.1", 1060 1070 "libc", 1061 1071 ] 1062 1072 ··· 1486 1496 version = "1.2.2" 1487 1497 source = "registry+https://github.com/rust-lang/crates.io-index" 1488 1498 checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" 1499 + 1500 + [[package]] 1501 + name = "encoding_rs" 1502 + version = "0.8.35" 1503 + source = "registry+https://github.com/rust-lang/crates.io-index" 1504 + checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" 1505 + dependencies = [ 1506 + "cfg-if", 1507 + ] 1489 1508 1490 1509 [[package]] 1491 1510 name = "endi" ··· 1957 1976 checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" 1958 1977 dependencies = [ 1959 1978 "cfg-if", 1979 + "js-sys", 1960 1980 "libc", 1961 1981 "wasi 0.11.1+wasi-snapshot-preview1", 1982 + "wasm-bindgen", 1962 1983 ] 1963 1984 1964 1985 [[package]] ··· 1968 1989 checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" 1969 1990 dependencies = [ 1970 1991 "cfg-if", 1992 + "js-sys", 1971 1993 "libc", 1972 1994 "r-efi", 1973 1995 "wasi 0.14.2+wasi-0.2.4", 1996 + "wasm-bindgen", 1974 1997 ] 1975 1998 1976 1999 [[package]] ··· 2173 2196 ] 2174 2197 2175 2198 [[package]] 2199 + name = "h2" 2200 + version = "0.4.10" 2201 + source = "registry+https://github.com/rust-lang/crates.io-index" 2202 + checksum = "a9421a676d1b147b16b82c9225157dc629087ef8ec4d5e2960f9437a90dac0a5" 2203 + dependencies = [ 2204 + "atomic-waker", 2205 + "bytes", 2206 + "fnv", 2207 + "futures-core", 2208 + "futures-sink", 2209 + "http", 2210 + "indexmap 2.9.0", 2211 + "slab", 2212 + "tokio", 2213 + "tokio-util", 2214 + "tracing", 2215 + ] 2216 + 2217 + [[package]] 2176 2218 name = "hashbrown" 2177 2219 version = "0.12.3" 2178 2220 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2298 2340 "bytes", 2299 2341 "futures-channel", 2300 2342 "futures-util", 2343 + "h2", 2301 2344 "http", 2302 2345 "http-body", 2303 2346 "httparse", ··· 2310 2353 ] 2311 2354 2312 2355 [[package]] 2356 + name = "hyper-rustls" 2357 + version = "0.27.7" 2358 + source = "registry+https://github.com/rust-lang/crates.io-index" 2359 + checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" 2360 + dependencies = [ 2361 + "http", 2362 + "hyper", 2363 + "hyper-util", 2364 + "rustls 0.23.28", 2365 + "rustls-pki-types", 2366 + "tokio", 2367 + "tokio-rustls", 2368 + "tower-service", 2369 + "webpki-roots 1.0.0", 2370 + ] 2371 + 2372 + [[package]] 2313 2373 name = "hyper-util" 2314 2374 version = "0.1.14" 2315 2375 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2328 2388 "percent-encoding", 2329 2389 "pin-project-lite", 2330 2390 "socket2", 2391 + "system-configuration", 2331 2392 "tokio", 2332 2393 "tower-service", 2333 2394 "tracing", 2395 + "windows-registry", 2334 2396 ] 2335 2397 2336 2398 [[package]] ··· 2816 2878 ] 2817 2879 2818 2880 [[package]] 2881 + name = "lru-slab" 2882 + version = "0.1.2" 2883 + source = "registry+https://github.com/rust-lang/crates.io-index" 2884 + checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" 2885 + 2886 + [[package]] 2819 2887 name = "mac" 2820 2888 version = "0.1.1" 2821 2889 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2837 2905 name = "manhunt-app" 2838 2906 version = "0.1.0" 2839 2907 dependencies = [ 2908 + "anyhow", 2840 2909 "chrono", 2841 2910 "futures", 2842 2911 "log", 2843 2912 "matchbox_socket", 2844 2913 "rand 0.9.1", 2845 2914 "rand_chacha 0.9.0", 2915 + "reqwest", 2846 2916 "rmp-serde", 2847 2917 "serde", 2848 2918 "serde_json", ··· 2857 2927 "tauri-plugin-store", 2858 2928 "tauri-specta", 2859 2929 "tokio", 2930 + "tokio-util", 2860 2931 "uuid", 2861 2932 ] 2862 2933 ··· 3991 4062 ] 3992 4063 3993 4064 [[package]] 4065 + name = "quinn" 4066 + version = "0.11.8" 4067 + source = "registry+https://github.com/rust-lang/crates.io-index" 4068 + checksum = "626214629cda6781b6dc1d316ba307189c85ba657213ce642d9c77670f8202c8" 4069 + dependencies = [ 4070 + "bytes", 4071 + "cfg_aliases", 4072 + "pin-project-lite", 4073 + "quinn-proto", 4074 + "quinn-udp", 4075 + "rustc-hash", 4076 + "rustls 0.23.28", 4077 + "socket2", 4078 + "thiserror 2.0.12", 4079 + "tokio", 4080 + "tracing", 4081 + "web-time", 4082 + ] 4083 + 4084 + [[package]] 4085 + name = "quinn-proto" 4086 + version = "0.11.12" 4087 + source = "registry+https://github.com/rust-lang/crates.io-index" 4088 + checksum = "49df843a9161c85bb8aae55f101bc0bac8bcafd637a620d9122fd7e0b2f7422e" 4089 + dependencies = [ 4090 + "bytes", 4091 + "getrandom 0.3.3", 4092 + "lru-slab", 4093 + "rand 0.9.1", 4094 + "ring", 4095 + "rustc-hash", 4096 + "rustls 0.23.28", 4097 + "rustls-pki-types", 4098 + "slab", 4099 + "thiserror 2.0.12", 4100 + "tinyvec", 4101 + "tracing", 4102 + "web-time", 4103 + ] 4104 + 4105 + [[package]] 4106 + name = "quinn-udp" 4107 + version = "0.5.12" 4108 + source = "registry+https://github.com/rust-lang/crates.io-index" 4109 + checksum = "ee4e529991f949c5e25755532370b8af5d114acae52326361d68d47af64aa842" 4110 + dependencies = [ 4111 + "cfg_aliases", 4112 + "libc", 4113 + "once_cell", 4114 + "socket2", 4115 + "tracing", 4116 + "windows-sys 0.52.0", 4117 + ] 4118 + 4119 + [[package]] 3994 4120 name = "quote" 3995 4121 version = "1.0.40" 3996 4122 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 4227 4353 dependencies = [ 4228 4354 "base64 0.22.1", 4229 4355 "bytes", 4356 + "encoding_rs", 4230 4357 "futures-core", 4231 4358 "futures-util", 4359 + "h2", 4232 4360 "http", 4233 4361 "http-body", 4234 4362 "http-body-util", 4235 4363 "hyper", 4364 + "hyper-rustls", 4236 4365 "hyper-util", 4237 4366 "js-sys", 4238 4367 "log", 4368 + "mime", 4239 4369 "percent-encoding", 4240 4370 "pin-project-lite", 4371 + "quinn", 4372 + "rustls 0.23.28", 4373 + "rustls-pki-types", 4241 4374 "serde", 4242 4375 "serde_json", 4243 4376 "serde_urlencoded", 4244 4377 "sync_wrapper", 4245 4378 "tokio", 4379 + "tokio-rustls", 4246 4380 "tokio-util", 4247 4381 "tower", 4248 4382 "tower-http", ··· 4252 4386 "wasm-bindgen-futures", 4253 4387 "wasm-streams", 4254 4388 "web-sys", 4389 + "webpki-roots 1.0.0", 4255 4390 ] 4256 4391 4257 4392 [[package]] ··· 4378 4513 checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f" 4379 4514 4380 4515 [[package]] 4516 + name = "rustc-hash" 4517 + version = "2.1.1" 4518 + source = "registry+https://github.com/rust-lang/crates.io-index" 4519 + checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" 4520 + 4521 + [[package]] 4381 4522 name = "rustc_version" 4382 4523 version = "0.4.1" 4383 4524 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 4449 4590 source = "registry+https://github.com/rust-lang/crates.io-index" 4450 4591 checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" 4451 4592 dependencies = [ 4593 + "web-time", 4452 4594 "zeroize", 4453 4595 ] 4454 4596 ··· 5129 5271 ] 5130 5272 5131 5273 [[package]] 5274 + name = "system-configuration" 5275 + version = "0.6.1" 5276 + source = "registry+https://github.com/rust-lang/crates.io-index" 5277 + checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" 5278 + dependencies = [ 5279 + "bitflags 2.9.1", 5280 + "core-foundation 0.9.4", 5281 + "system-configuration-sys", 5282 + ] 5283 + 5284 + [[package]] 5285 + name = "system-configuration-sys" 5286 + version = "0.6.0" 5287 + source = "registry+https://github.com/rust-lang/crates.io-index" 5288 + checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" 5289 + dependencies = [ 5290 + "core-foundation-sys", 5291 + "libc", 5292 + ] 5293 + 5294 + [[package]] 5132 5295 name = "system-deps" 5133 5296 version = "6.2.2" 5134 5297 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 5148 5311 checksum = "1e59c1f38e657351a2e822eadf40d6a2ad4627b9c25557bc1180ec1b3295ef82" 5149 5312 dependencies = [ 5150 5313 "bitflags 2.9.1", 5151 - "core-foundation", 5314 + "core-foundation 0.10.1", 5152 5315 "core-graphics", 5153 5316 "crossbeam-channel", 5154 5317 "dispatch", ··· 5724 5887 ] 5725 5888 5726 5889 [[package]] 5890 + name = "tokio-rustls" 5891 + version = "0.26.2" 5892 + source = "registry+https://github.com/rust-lang/crates.io-index" 5893 + checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" 5894 + dependencies = [ 5895 + "rustls 0.23.28", 5896 + "tokio", 5897 + ] 5898 + 5899 + [[package]] 5727 5900 name = "tokio-stream" 5728 5901 version = "0.1.17" 5729 5902 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 6308 6481 ] 6309 6482 6310 6483 [[package]] 6484 + name = "web-time" 6485 + version = "1.1.0" 6486 + source = "registry+https://github.com/rust-lang/crates.io-index" 6487 + checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" 6488 + dependencies = [ 6489 + "js-sys", 6490 + "wasm-bindgen", 6491 + ] 6492 + 6493 + [[package]] 6311 6494 name = "webkit2gtk" 6312 6495 version = "2.0.1" 6313 6496 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 6368 6551 checksum = "b6c71e40d7d2c34a5106301fb632274ca37242cd0c9d3e64dbece371a40a2d87" 6369 6552 dependencies = [ 6370 6553 "webpki", 6554 + ] 6555 + 6556 + [[package]] 6557 + name = "webpki-roots" 6558 + version = "1.0.0" 6559 + source = "registry+https://github.com/rust-lang/crates.io-index" 6560 + checksum = "2853738d1cc4f2da3a225c18ec6c3721abb31961096e9dbf5ab35fa88b19cfdb" 6561 + dependencies = [ 6562 + "rustls-pki-types", 6371 6563 ] 6372 6564 6373 6565 [[package]] ··· 6743 6935 dependencies = [ 6744 6936 "windows-core", 6745 6937 "windows-link", 6938 + ] 6939 + 6940 + [[package]] 6941 + name = "windows-registry" 6942 + version = "0.5.2" 6943 + source = "registry+https://github.com/rust-lang/crates.io-index" 6944 + checksum = "b3bab093bdd303a1240bb99b8aba8ea8a69ee19d34c9e2ef9594e708a4878820" 6945 + dependencies = [ 6946 + "windows-link", 6947 + "windows-result", 6948 + "windows-strings", 6746 6949 ] 6747 6950 6748 6951 [[package]]
+6 -3
TODO.md
··· 2 2 3 3 ## Ben 4 4 5 - - [ ] Transport : Packet splitting 6 - - [ ] Transport : Handle Errors 7 - - [ ] Transport : Mark game started on client 5 + - [x] Transport : Packet splitting 6 + - [x] Transport : Handle Errors 7 + - [x] Transport : Mark game started on client 8 + - [x] API : Command to check if a game exists and is open for fast error checking 9 + - [x] Transport : Switch to burst message processing for less time in the 10 + critical path 8 11 - [ ] State : Event history tracking 9 12 - [ ] State : Post game sync 10 13 - [ ] API : Handling Profile Syncing
+3
backend/Cargo.toml
··· 38 38 tauri-plugin-log = "2" 39 39 tauri-plugin-notification = "2" 40 40 log = "0.4.27" 41 + tokio-util = "0.7.15" 42 + anyhow = "1.0.98" 43 + reqwest = { version = "0.12.20", default-features = false, features = ["charset", "http2", "rustls-tls", "system-proxy"] }
+5
backend/src/game/events.rs
··· 17 17 /// Contains location history of the given player, used after the game to sync location 18 18 /// histories 19 19 PostGameSync(Id, Vec<Location>), 20 + /// A player has been disconnected and removed from the game (because of error or otherwise). 21 + /// The player should be removed from all state 22 + DroppedPlayer(Id), 23 + /// The underlying transport has disconnected 24 + TransportDisconnect, 20 25 }
+50 -21
backend/src/game/mod.rs
··· 3 3 use powerups::PowerUpType; 4 4 pub use settings::GameSettings; 5 5 use std::{collections::HashMap, sync::Arc, time::Duration}; 6 + use tokio_util::sync::CancellationToken; 6 7 use uuid::Uuid; 7 8 8 9 use tokio::{sync::RwLock, time::MissedTickBehavior}; ··· 13 14 mod settings; 14 15 mod state; 15 16 mod transport; 17 + 18 + use crate::prelude::*; 16 19 17 20 pub use location::{Location, LocationService}; 18 21 pub use state::GameState; ··· 31 34 transport: Arc<T>, 32 35 location: L, 33 36 interval: Duration, 37 + transport_cancel_token: CancellationToken, 34 38 } 35 39 36 40 impl<L: LocationService, T: Transport> Game<L, T> { ··· 41 45 settings: GameSettings, 42 46 transport: Arc<T>, 43 47 location: L, 48 + transport_cancel_token: CancellationToken, 44 49 ) -> Self { 45 50 let state = GameState::new(settings, my_id, initial_caught_state); 46 51 47 52 Self { 48 53 transport, 54 + transport_cancel_token, 49 55 location, 50 56 interval, 51 57 state: RwLock::new(state), ··· 104 110 } 105 111 } 106 112 107 - async fn consume_event(&self, event: GameEvent) { 108 - let mut state = self.state.write().await; 109 - 113 + async fn consume_event(&self, state: &mut GameState, event: GameEvent) -> Result { 110 114 match event { 111 115 GameEvent::Ping(player_ping) => state.add_ping(player_ping), 112 116 GameEvent::ForcePing(target, display) => { 113 117 if target != state.id { 114 - return; 118 + return Ok(()); 115 119 } 116 120 117 121 let ping = if let Some(display) = display { ··· 129 133 GameEvent::PlayerCaught(player) => { 130 134 state.mark_caught(player); 131 135 state.remove_ping(player); 136 + } 137 + GameEvent::DroppedPlayer(id) => { 138 + state.remove_player(id); 139 + } 140 + GameEvent::TransportDisconnect => { 141 + bail!("Transport disconnected"); 132 142 } 133 143 GameEvent::PostGameSync(_, _locations) => {} 134 144 } 145 + 146 + Ok(()) 135 147 } 136 148 137 149 /// Perform a tick for a specific moment in time 138 - async fn tick(&self, now: UtcDT) { 139 - let mut state = self.state.write().await; 140 - 150 + async fn tick(&self, state: &mut GameState, now: UtcDT) { 141 151 // Push to location history 142 152 if let Some(location) = self.location.get_loc() { 143 153 state.push_loc(location); ··· 186 196 187 197 #[cfg(test)] 188 198 pub async fn force_tick(&self, now: UtcDT) { 189 - self.tick(now).await; 199 + let mut state = self.state.write().await; 200 + self.tick(&mut state, now).await; 201 + } 202 + 203 + pub fn quit_game(&self) { 204 + self.transport_cancel_token.cancel(); 190 205 } 191 206 192 207 /// Main loop of the game, handles ticking and receiving messages from [Transport]. 193 - pub async fn main_loop(&self) { 208 + pub async fn main_loop(&self) -> Result { 194 209 let mut interval = tokio::time::interval(self.interval); 195 210 196 211 interval.set_missed_tick_behavior(MissedTickBehavior::Delay); 197 212 198 - loop { 213 + let res = 'game: loop { 199 214 tokio::select! { 215 + biased; 200 216 201 - biased; 217 + events = self.transport.receive_messages() => { 218 + let mut state = self.state.write().await; 219 + for event in events { 220 + if let Err(why) = self.consume_event(&mut state, event).await { 221 + break 'game Err(why); 222 + } 223 + } 202 224 203 - Some(msg) = self.transport.receive_message() => { 204 - self.consume_event(msg).await; 205 - // TODO: Check all caught, end game 225 + if state.should_end() { 226 + break Ok(()); 227 + } 206 228 } 207 229 208 230 _ = interval.tick() => { 209 - let now = Utc::now(); 210 - self.tick(now).await; 231 + let mut state = self.state.write().await; 232 + self.tick(&mut state, Utc::now()).await; 211 233 } 212 - }; 213 - } 234 + } 235 + }; 236 + 237 + self.transport_cancel_token.cancel(); 238 + 239 + res 214 240 } 215 241 } 216 242 ··· 232 258 } 233 259 234 260 impl Transport for MockTransport { 235 - async fn receive_message(&self) -> Option<GameEvent> { 261 + async fn receive_messages(&self) -> impl Iterator<Item = GameEvent> { 236 262 let mut rx = self.rx.lock().await; 237 - rx.recv().await 263 + let mut buf = Vec::with_capacity(20); 264 + rx.recv_many(&mut buf, 20).await; 265 + buf.into_iter() 238 266 } 239 267 240 268 async fn send_message(&self, msg: GameEvent) { ··· 301 329 settings.clone(), 302 330 Arc::new(transport), 303 331 location, 332 + CancellationToken::new(), 304 333 ); 305 334 306 335 (id as u32, Arc::new(game)) ··· 319 348 for game in self.games.values() { 320 349 let game = game.clone(); 321 350 tokio::spawn(async move { 322 - game.main_loop().await; 351 + game.main_loop().await.expect("Game Start Fail"); 323 352 }); 324 353 yield_now().await; 325 354 }
+11
backend/src/game/state.rs
··· 240 240 self.pings.get(&player) 241 241 } 242 242 243 + /// Check if the game should be ended (due to all players being caught) 244 + pub fn should_end(&self) -> bool { 245 + self.caught_state.values().all(|v| *v) 246 + } 247 + 243 248 /// Remove a ping from the map 244 249 pub fn remove_ping(&mut self, player: Id) -> Option<PlayerPing> { 245 250 self.pings.remove(&player) ··· 281 286 /// Create a [PlayerPing] with the latest location as another player 282 287 pub fn create_ping(&self, id: Id) -> Option<PlayerPing> { 283 288 self.get_loc().map(|loc| PlayerPing::new(*loc, id, self.id)) 289 + } 290 + 291 + /// Remove a player from the game by their ID number 292 + pub fn remove_player(&mut self, id: Id) { 293 + self.pings.remove(&id); 294 + self.caught_state.remove(&id); 284 295 } 285 296 286 297 /// Player has gotten a powerup, rolls to see which powerup and stores it
+1 -1
backend/src/game/transport.rs
··· 2 2 3 3 pub trait Transport { 4 4 /// Receive an event 5 - async fn receive_message(&self) -> Option<GameEvent>; 5 + async fn receive_messages(&self) -> impl Iterator<Item = GameEvent>; 6 6 /// Send an event 7 7 async fn send_message(&self, msg: GameEvent); 8 8 }
+110 -32
backend/src/lib.rs
··· 9 9 use game::{Game as BaseGame, GameSettings, GameState}; 10 10 use lobby::{Lobby, LobbyState, StartGameInfo}; 11 11 use location::TauriLocation; 12 + use log::{error, warn}; 12 13 use profile::PlayerProfile; 14 + use reqwest::StatusCode; 13 15 use serde::{Deserialize, Serialize}; 14 16 use tauri::{AppHandle, Manager, State}; 15 - use tauri_specta::collect_commands; 17 + use tauri_specta::{collect_commands, collect_events, Event}; 16 18 use tokio::sync::RwLock; 17 19 use transport::MatchboxTransport; 18 20 use uuid::Uuid; 19 21 22 + mod prelude { 23 + pub use anyhow::{anyhow, bail, Error as AnyhowError}; 24 + pub use std::result::Result as StdResult; 25 + 26 + pub type Result<T = (), E = AnyhowError> = StdResult<T, E>; 27 + } 28 + 20 29 type Game = BaseGame<TauriLocation, MatchboxTransport>; 21 30 22 31 enum AppState { ··· 37 46 38 47 const GAME_TICK_RATE: Duration = Duration::from_secs(1); 39 48 40 - const fn server_url() -> &'static str { 49 + pub const fn server_url() -> &'static str { 41 50 if let Some(url) = option_env!("APP_SERVER_URL") { 42 51 url 43 52 } else { ··· 45 54 } 46 55 } 47 56 57 + #[derive(Serialize, Deserialize, Clone, Debug, specta::Type, tauri_specta::Event)] 58 + struct ChangeScreen(AppScreen); 59 + 48 60 impl AppState { 49 61 pub fn start_game(&mut self, app: AppHandle, my_id: Uuid, start: StartGameInfo) { 50 62 if let AppState::Lobby(lobby) = self { ··· 57 69 start.settings, 58 70 transport, 59 71 location, 72 + lobby.clone_cancel(), 60 73 )); 61 74 *self = AppState::Game(game.clone()); 62 75 tokio::spawn(async move { 63 - game.main_loop().await; 76 + let res = game.main_loop().await; 77 + let app2 = app.clone(); 78 + let state_handle = app.state::<AppStateHandle>(); 79 + let mut state = state_handle.write().await; 80 + match res { 81 + Ok(_) => { 82 + // TODO: Post game screen, etc here. Game::main_loop should return smth 83 + // like GameHistory for playback and serialization 84 + state.quit_game_or_lobby(app2); 85 + } 86 + Err(why) => { 87 + error!("Game Error: {why:?}"); 88 + state.quit_game_or_lobby(app2); 89 + } 90 + } 64 91 }); 65 92 } 66 93 } ··· 93 120 } 94 121 } 95 122 123 + fn emit_screen_change(app: &AppHandle, screen: AppScreen) { 124 + if let Err(why) = ChangeScreen(screen).emit(app) { 125 + warn!("Error emitting screen change: {why:?}"); 126 + } 127 + } 128 + 96 129 pub fn start_lobby( 97 130 &mut self, 98 131 join_code: Option<String>, ··· 110 143 settings, 111 144 )); 112 145 *self = AppState::Lobby(lobby.clone()); 146 + let app2 = app.clone(); 113 147 tokio::spawn(async move { 114 - let (my_id, start) = lobby.open().await; 115 - let app_game = app.clone(); 116 - let state_handle = app.state::<AppStateHandle>(); 148 + let res = lobby.open().await; 149 + let app_game = app2.clone(); 150 + let state_handle = app2.state::<AppStateHandle>(); 117 151 let mut state = state_handle.write().await; 118 - state.start_game(app_game, my_id, start); 152 + match res { 153 + Ok((my_id, start)) => { 154 + state.start_game(app_game, my_id, start); 155 + } 156 + Err(why) => { 157 + error!("Lobby Error: {why:?}"); 158 + state.quit_game_or_lobby(app_game); 159 + } 160 + } 119 161 }); 162 + Self::emit_screen_change(&app, AppScreen::Lobby); 120 163 } 121 164 } 165 + 166 + pub fn quit_game_or_lobby(&mut self, app: AppHandle) { 167 + let profile = match self { 168 + AppState::Setup => None, 169 + AppState::Menu(_) => { 170 + warn!("Already on menu!"); 171 + return; 172 + } 173 + AppState::Lobby(lobby) => { 174 + lobby.quit_lobby(); 175 + Some(lobby.self_profile.clone()) 176 + } 177 + AppState::Game(game) => { 178 + game.quit_game(); 179 + PlayerProfile::load_from_store(&app) 180 + } 181 + }; 182 + let screen = if let Some(profile) = profile { 183 + *self = AppState::Menu(profile); 184 + AppScreen::Menu 185 + } else { 186 + *self = AppState::Setup; 187 + AppScreen::Setup 188 + }; 189 + 190 + Self::emit_screen_change(&app, screen); 191 + } 122 192 } 123 193 124 194 use std::result::Result as StdResult; ··· 153 223 /// Quit a running game or leave a lobby 154 224 async fn quit_game_or_lobby(app: AppHandle, state: State<'_, AppStateHandle>) -> Result { 155 225 let mut state = state.write().await; 156 - let profile = match &*state { 157 - AppState::Setup => Err("Invalid Screen".to_string()), 158 - AppState::Menu(_) => Err("Already In Menu".to_string()), 159 - AppState::Lobby(_) | AppState::Game(_) => Ok(PlayerProfile::load_from_store(&app)), 160 - }?; 161 - if let Some(profile) = profile { 162 - *state = AppState::Menu(profile); 163 - } else { 164 - *state = AppState::Setup; 165 - } 226 + state.quit_game_or_lobby(app); 166 227 Ok(()) 167 228 } 168 229 ··· 175 236 let state = state.read().await; 176 237 let profile = state.get_menu()?; 177 238 Ok(profile.clone()) 239 + } 240 + 241 + #[tauri::command] 242 + #[specta::specta] 243 + /// (Screen: Menu) Check if a room code is valid to join, use this before starting a game 244 + /// for faster error checking. 245 + async fn check_room_code(code: &str) -> Result<bool, String> { 246 + let url = format!("{}/room_exists/{code}", server_url()); 247 + reqwest::get(url) 248 + .await 249 + .map(|resp| resp.status() == StatusCode::OK) 250 + .map_err(|err| err.to_string()) 178 251 } 179 252 180 253 #[tauri::command] ··· 280 353 } 281 354 282 355 pub fn mk_specta() -> tauri_specta::Builder { 283 - tauri_specta::Builder::<tauri::Wry>::new().commands(collect_commands![ 284 - start_lobby, 285 - get_profile, 286 - quit_game_or_lobby, 287 - get_current_screen, 288 - update_profile, 289 - get_lobby_state, 290 - host_update_settings, 291 - switch_teams, 292 - host_start_game, 293 - mark_caught, 294 - grab_powerup, 295 - use_powerup, 296 - ]) 356 + tauri_specta::Builder::<tauri::Wry>::new() 357 + .commands(collect_commands![ 358 + start_lobby, 359 + get_profile, 360 + quit_game_or_lobby, 361 + get_current_screen, 362 + update_profile, 363 + get_lobby_state, 364 + host_update_settings, 365 + switch_teams, 366 + host_start_game, 367 + mark_caught, 368 + grab_powerup, 369 + use_powerup, 370 + check_room_code, 371 + ]) 372 + .events(collect_events![ChangeScreen]) 297 373 } 298 374 299 375 #[cfg_attr(mobile, tauri::mobile_entry_point)] ··· 310 386 .plugin(tauri_plugin_store::Builder::default().build()) 311 387 .invoke_handler(builder.invoke_handler()) 312 388 .manage(state) 313 - .setup(|app| { 389 + .setup(move |app| { 390 + builder.mount_events(app); 391 + 314 392 let handle = app.handle().clone(); 315 393 tauri::async_runtime::spawn(async move { 316 394 if let Some(profile) = PlayerProfile::load_from_store(&handle) {
+68 -20
backend/src/lobby.rs
··· 1 - use std::{collections::HashMap, sync::Arc}; 1 + use std::{collections::HashMap, sync::Arc, time::Duration}; 2 2 3 + use log::warn; 3 4 use serde::{Deserialize, Serialize}; 4 5 use tokio::sync::Mutex; 6 + use tokio_util::sync::CancellationToken; 5 7 use uuid::Uuid; 6 8 7 9 use crate::{ 8 10 game::GameSettings, 11 + prelude::*, 9 12 profile::PlayerProfile, 13 + server_url, 10 14 transport::{MatchboxTransport, TransportMessage}, 11 15 }; 12 16 ··· 40 44 41 45 pub struct Lobby { 42 46 is_host: bool, 47 + join_code: String, 43 48 pub self_profile: PlayerProfile, 44 49 state: Mutex<LobbyState>, 45 50 transport: Arc<MatchboxTransport>, 51 + cancel_token: CancellationToken, 46 52 } 47 53 48 54 impl Lobby { ··· 53 59 profile: PlayerProfile, 54 60 settings: GameSettings, 55 61 ) -> Self { 62 + let cancel_token = CancellationToken::new(); 56 63 Self { 57 64 transport: Arc::new(MatchboxTransport::new(&format!( 58 65 "{ws_url_base}/{join_code}{}", 59 66 if host { "?create" } else { "" } 60 67 ))), 68 + cancel_token, 61 69 is_host: host, 62 70 self_profile: profile, 71 + join_code: join_code.to_string(), 63 72 state: Mutex::new(LobbyState { 64 73 teams: HashMap::with_capacity(5), 65 74 join_code: join_code.to_string(), ··· 84 93 state.self_seeker = seeker; 85 94 drop(state); 86 95 self.transport 87 - .send_transport_message( 88 - None, 89 - TransportMessage::Lobby(LobbyMessage::PlayerSwitch(seeker)), 90 - ) 96 + .send_transport_message(None, LobbyMessage::PlayerSwitch(seeker).into()) 91 97 .await; 92 98 } 93 99 ··· 97 103 let mut state = self.state.lock().await; 98 104 state.settings = new_settings.clone(); 99 105 drop(state); 100 - let msg = TransportMessage::Lobby(LobbyMessage::HostPush(new_settings)); 101 - self.transport.send_transport_message(None, msg).await; 106 + let msg = LobbyMessage::HostPush(new_settings); 107 + self.send_transport_message(None, msg).await; 102 108 } 103 109 } 104 110 111 + async fn send_transport_message(&self, id: Option<Uuid>, msg: LobbyMessage) { 112 + self.transport.send_transport_message(id, msg.into()).await 113 + } 114 + 115 + async fn singaling_mark_started(&self) -> Result { 116 + let url = format!("{}/mark_started/{}", server_url(), &self.join_code); 117 + let client = reqwest::Client::builder().build()?; 118 + client.post(url).send().await?.error_for_status()?; 119 + Ok(()) 120 + } 121 + 105 122 /// (Host) Start the game 106 123 pub async fn start_game(&self) { 107 124 if self.is_host { ··· 113 130 settings: state.settings.clone(), 114 131 initial_caught_state: state.teams.clone(), 115 132 }; 116 - let msg = TransportMessage::Lobby(LobbyMessage::StartGame(start_game_info)); 117 - self.transport.send_transport_message(None, msg).await; 133 + drop(state); 134 + let msg = LobbyMessage::StartGame(start_game_info); 135 + self.send_transport_message(None, msg).await; 136 + if let Err(why) = self.singaling_mark_started().await { 137 + warn!("Failed to tell signalling server that the match started: {why:?}"); 138 + } 118 139 } 119 140 } 120 141 } 121 142 122 - pub async fn open(&self) -> (Uuid, StartGameInfo) { 143 + pub fn clone_cancel(&self) -> CancellationToken { 144 + self.cancel_token.clone() 145 + } 146 + 147 + pub fn quit_lobby(&self) { 148 + self.cancel_token.cancel(); 149 + } 150 + 151 + pub async fn open(&self) -> Result<(Uuid, StartGameInfo)> { 123 152 let transport_inner = self.transport.clone(); 124 - tokio::spawn(async move { transport_inner.transport_loop().await }); 153 + tokio::spawn({ 154 + let cancel = self.cancel_token.clone(); 155 + async move { transport_inner.transport_loop(cancel).await } 156 + }); 125 157 126 - loop { 127 - if let Some((peer, msg)) = self.transport.recv_transport_message().await { 158 + let mut interval = tokio::time::interval(Duration::from_secs(1)); 159 + 160 + let res = 'lobby: loop { 161 + interval.tick().await; 162 + 163 + let msgs = self.transport.recv_transport_messages().await; 164 + 165 + for (peer, msg) in msgs { 128 166 match msg { 167 + TransportMessage::Disconnected => { 168 + break 'lobby Err(anyhow!( 169 + "Transport disconnected before lobby could start game" 170 + )); 171 + } 129 172 TransportMessage::Game(game_event) => { 130 173 eprintln!("Peer {peer:?} sent a GameEvent: {game_event:?}"); 131 174 } 132 - TransportMessage::Lobby(lobby_message) => match lobby_message { 175 + TransportMessage::Lobby(lobby_message) => match *lobby_message { 133 176 LobbyMessage::PlayerSync(player_profile) => { 134 177 let mut state = self.state.lock().await; 135 178 state.profiles.insert(peer, player_profile); ··· 139 182 state.settings = game_settings; 140 183 } 141 184 LobbyMessage::StartGame(start_game_info) => { 142 - break ( 185 + break 'lobby Ok(( 143 186 self.transport 144 187 .get_my_id() 145 188 .await 146 189 .expect("Error getting self ID"), 147 190 start_game_info, 148 - ); 191 + )); 149 192 } 150 193 LobbyMessage::PlayerSwitch(seeker) => { 151 194 let mut state = self.state.lock().await; ··· 157 200 let mut state = self.state.lock().await; 158 201 state.teams.insert(peer, false); 159 202 drop(state); 160 - let msg = TransportMessage::Lobby(msg); 161 - self.transport.send_transport_message(Some(peer), msg).await; 203 + self.send_transport_message(Some(peer), msg).await; 162 204 if self.is_host { 163 205 let state = self.state.lock().await; 164 206 let msg = LobbyMessage::HostPush(state.settings.clone()); 165 207 drop(state); 166 - let msg = TransportMessage::Lobby(msg); 167 - self.transport.send_transport_message(Some(peer), msg).await; 208 + self.send_transport_message(Some(peer), msg).await; 168 209 } 169 210 } 170 211 TransportMessage::PeerDisconnect => { ··· 172 213 state.profiles.remove(&peer); 173 214 state.teams.remove(&peer); 174 215 } 216 + TransportMessage::Seq(_) => {} 175 217 } 176 218 } 219 + }; 220 + 221 + if res.is_err() { 222 + self.cancel_token.cancel(); 177 223 } 224 + 225 + res 178 226 } 179 227 }
+1 -1
backend/src/profile.rs
··· 2 2 use tauri::AppHandle; 3 3 use tauri_plugin_store::StoreExt; 4 4 5 - #[derive(Clone, Debug, Serialize, Deserialize, specta::Type)] 5 + #[derive(Clone, Default, Debug, Serialize, Deserialize, specta::Type)] 6 6 pub struct PlayerProfile { 7 7 display_name: String, 8 8 pfp_base64: Option<String>,
+210 -44
backend/src/transport.rs
··· 1 - use std::{collections::HashSet, time::Duration}; 1 + use std::{ 2 + collections::{HashMap, HashSet}, 3 + time::Duration, 4 + }; 2 5 6 + use anyhow::Context; 3 7 use futures::FutureExt; 8 + use log::error; 4 9 use matchbox_socket::{PeerId, PeerState, WebRtcSocket}; 5 10 use serde::{Deserialize, Serialize}; 6 11 use tokio::sync::{Mutex, RwLock}; 12 + use tokio_util::sync::CancellationToken; 7 13 use uuid::Uuid; 8 14 9 15 use crate::{ 10 16 game::{GameEvent, Transport}, 11 17 lobby::LobbyMessage, 18 + prelude::*, 12 19 }; 13 20 21 + #[derive(Serialize, Deserialize, Debug, Clone)] 22 + pub struct TransportChunk { 23 + id: u64, 24 + current: usize, 25 + total: usize, 26 + data: Vec<u8>, 27 + } 28 + 14 29 #[derive(Debug, Serialize, Deserialize, Clone)] 15 30 pub enum TransportMessage { 16 31 /// Message related to the actual game 17 - Game(GameEvent), 32 + /// Boxed for space reasons 33 + Game(Box<GameEvent>), 18 34 /// Message related to the pre-game lobby 19 - Lobby(LobbyMessage), 35 + Lobby(Box<LobbyMessage>), 20 36 /// Internal message when peer connects 21 37 PeerConnect, 22 38 /// Internal message when peer disconnects 23 39 PeerDisconnect, 40 + /// Event sent when the transport gets disconnected, used to help consumers know when to stop 41 + /// consuming messages 42 + Disconnected, 43 + /// Internal message for packet chunking 44 + Seq(TransportChunk), 45 + } 46 + 47 + // Max packet size according to: https://github.com/johanhelsing/matchbox/issues/272 48 + const MAX_PACKET_SIZE: usize = 65535; 49 + 50 + // Align packets with a bit of extra space for [TransportMessage::Seq] header 51 + const PACKET_ALIGNMENT: usize = MAX_PACKET_SIZE - 128; 52 + 53 + impl TransportMessage { 54 + pub fn serialize(&self) -> Vec<u8> { 55 + rmp_serde::to_vec(self).expect("Failed to encode") 56 + } 57 + 58 + pub fn deserialize(data: &[u8]) -> Result<Self> { 59 + rmp_serde::from_slice(data).context("While deserializing message") 60 + } 61 + 62 + pub fn from_packets(packets: impl Iterator<Item = Box<[u8]>>) -> Result<Self> { 63 + let full_data = packets.flatten().collect::<Box<[u8]>>(); 64 + Self::deserialize(&full_data).context("While decoding a multi-part message") 65 + } 66 + 67 + pub fn to_packets(&self) -> Vec<Vec<u8>> { 68 + let bytes = self.serialize(); 69 + if bytes.len() > MAX_PACKET_SIZE { 70 + let id = rand::random_range(0..u64::MAX); 71 + let packets_needed = bytes.len().div_ceil(PACKET_ALIGNMENT); 72 + let rem = bytes.len() % PACKET_ALIGNMENT; 73 + let bytes = bytes.into_boxed_slice(); 74 + (0..packets_needed) 75 + .map(|idx| { 76 + let start = PACKET_ALIGNMENT * idx; 77 + let end = if idx == packets_needed - 1 { 78 + start + rem 79 + } else { 80 + PACKET_ALIGNMENT * (idx + 1) 81 + }; 82 + let data = bytes[start..end].to_vec(); 83 + let chunk = TransportChunk { 84 + id, 85 + current: idx, 86 + total: packets_needed, 87 + data, 88 + }; 89 + TransportMessage::Seq(chunk).serialize() 90 + }) 91 + .collect() 92 + } else { 93 + vec![bytes] 94 + } 95 + } 96 + } 97 + 98 + impl From<GameEvent> for TransportMessage { 99 + fn from(v: GameEvent) -> Self { 100 + Self::Game(Box::new(v)) 101 + } 102 + } 103 + 104 + impl From<LobbyMessage> for TransportMessage { 105 + fn from(v: LobbyMessage) -> Self { 106 + Self::Lobby(Box::new(v)) 107 + } 24 108 } 25 109 26 110 type OutgoingMsgPair = (Option<Uuid>, TransportMessage); ··· 59 143 .expect("Failed to add to outgoing queue"); 60 144 } 61 145 62 - pub async fn recv_transport_message(&self) -> Option<IncomingMsgPair> { 146 + pub async fn recv_transport_messages(&self) -> Vec<IncomingMsgPair> { 63 147 let mut incoming_rx = self.incoming.1.lock().await; 64 - incoming_rx.recv().await 148 + let mut buffer = Vec::with_capacity(60); 149 + incoming_rx.recv_many(&mut buffer, 60).await; 150 + buffer 65 151 } 66 152 67 153 pub async fn get_my_id(&self) -> Option<Uuid> { 68 154 *self.my_id.read().await 69 155 } 70 156 71 - pub async fn transport_loop(&self) { 157 + async fn push_incoming(&self, id: Uuid, msg: TransportMessage) { 158 + self.incoming 159 + .0 160 + .send((id, msg)) 161 + .await 162 + .expect("Failed to push to incoming queue"); 163 + } 164 + 165 + async fn handle_send( 166 + &self, 167 + socket: &mut WebRtcSocket, 168 + all_peers: &HashSet<PeerId>, 169 + messages: impl Iterator<Item = OutgoingMsgPair>, 170 + ) { 171 + let packets = messages.flat_map(|(id, msg)| { 172 + msg.to_packets() 173 + .into_iter() 174 + .map(move |packet| (id, packet.into_boxed_slice())) 175 + }); 176 + 177 + for (peer, packet) in packets { 178 + if let Some(peer) = peer { 179 + let channel = socket.channel_mut(0); 180 + channel.send(packet, PeerId(peer)); 181 + } else { 182 + let channel = socket.channel_mut(0); 183 + 184 + for peer in all_peers.iter() { 185 + // TODO: Any way around having to clone here? 186 + let data = packet.clone(); 187 + channel.send(data, *peer); 188 + } 189 + } 190 + } 191 + } 192 + 193 + pub async fn transport_loop(&self, cancel: CancellationToken) { 72 194 let (mut socket, loop_fut) = WebRtcSocket::new_reliable(&self.ws_url); 73 195 74 196 let loop_fut = loop_fut.fuse(); ··· 79 201 80 202 let mut timer = tokio::time::interval(Duration::from_millis(100)); 81 203 204 + let mut partial_packets = 205 + HashMap::<u64, (Uuid, HashMap<usize, Option<Vec<u8>>>)>::with_capacity(3); 206 + 82 207 loop { 83 208 for (peer, state) in socket.update_peers() { 84 209 let msg = match state { ··· 91 216 TransportMessage::PeerDisconnect 92 217 } 93 218 }; 94 - self.incoming 95 - .0 96 - .send((peer.0, msg)) 97 - .await 98 - .expect("Failed to push to incoming queue"); 219 + self.push_incoming(peer.0, msg).await; 99 220 } 100 221 101 - for (peer, data) in socket.channel_mut(0).receive() { 102 - if let Ok(msg) = rmp_serde::from_slice(&data) { 103 - self.incoming 104 - .0 105 - .send((peer.0, msg)) 106 - .await 107 - .expect("Failed to push to incoming queue"); 222 + let messages = socket.channel_mut(0).receive(); 223 + 224 + let mut messages = messages 225 + .into_iter() 226 + .filter_map(|(id, data)| { 227 + let msg = TransportMessage::deserialize(&data).ok(); 228 + 229 + if let Some(TransportMessage::Seq(TransportChunk { 230 + id: multipart_id, 231 + current, 232 + total, 233 + data, 234 + })) = msg 235 + { 236 + if let Some((_, map)) = partial_packets.get_mut(&multipart_id) { 237 + map.insert(current, Some(data)); 238 + } else { 239 + let mut map = HashMap::from_iter((0..total).map(|idx| (idx, None))); 240 + map.insert(current, Some(data)); 241 + partial_packets.insert(multipart_id, (id.0, map)); 242 + } 243 + None 244 + } else { 245 + msg.map(|msg| (id.0, msg)) 246 + } 247 + }) 248 + .collect::<Vec<_>>(); 249 + 250 + let complete_messages = partial_packets 251 + .keys() 252 + .copied() 253 + .filter(|id| { 254 + partial_packets 255 + .get(id) 256 + .is_some_and(|(_, v)| v.values().all(Option::is_some)) 257 + }) 258 + .collect::<Vec<_>>(); 259 + 260 + for id in complete_messages { 261 + let (peer, packet_map) = partial_packets.remove(&id).unwrap(); 262 + 263 + let res = TransportMessage::from_packets( 264 + packet_map 265 + .into_values() 266 + .map(|v| v.unwrap().into_boxed_slice()), 267 + ); 268 + 269 + match res { 270 + Ok(msg) => messages.push((peer, msg)), 271 + Err(why) => error!("Error receiving message: {why:?}"), 108 272 } 109 273 } 110 274 275 + let push_iter = self 276 + .incoming 277 + .0 278 + .reserve_many(messages.len()) 279 + .await 280 + .expect("Couldn't reserve space"); 281 + 282 + for (sender, msg) in push_iter.zip(messages.into_iter()) { 283 + sender.send(msg); 284 + } 285 + 111 286 if my_id.is_none() { 112 287 if let Some(new_id) = socket.id() { 113 288 my_id = Some(new_id.0); ··· 117 292 118 293 let mut outgoing_rx = self.outgoing.1.lock().await; 119 294 295 + let mut buffer = Vec::with_capacity(30); 296 + 120 297 tokio::select! { 298 + 299 + _ = cancel.cancelled() => { 300 + socket.close(); 301 + 302 + } 303 + 121 304 _ = timer.tick() => { 122 305 // Transport Tick 123 306 continue; 124 307 } 125 308 126 - Some((peer, msg)) = outgoing_rx.recv() => { 127 - let encoded = rmp_serde::to_vec(&msg).unwrap(); 128 - 129 - if let Some(peer) = peer { 130 - let channel = socket.channel_mut(0); 131 - let data = encoded.into_boxed_slice(); 132 - channel.send(data, PeerId(peer)); 133 - } else { 134 - // Send to self as well 135 - if let Some(myself) = my_id { 136 - self.incoming.0.send((myself, msg)).await.expect("Failed to push to incoming queue"); 137 - } 138 - let channel = socket.channel_mut(0); 139 - 140 - for peer in all_peers.iter() { 141 - // TODO: Any way around having to clone here? 142 - let data = encoded.clone().into_boxed_slice(); 143 - channel.send(data, *peer); 144 - } 145 - } 309 + _ = outgoing_rx.recv_many(&mut buffer, 30) => { 310 + self.handle_send(&mut socket, &all_peers, buffer.drain(..)).await; 146 311 } 147 312 148 313 _ = &mut loop_fut => { ··· 155 320 } 156 321 157 322 impl Transport for MatchboxTransport { 158 - async fn receive_message(&self) -> Option<GameEvent> { 159 - self.recv_transport_message() 323 + async fn receive_messages(&self) -> impl Iterator<Item = GameEvent> { 324 + self.recv_transport_messages() 160 325 .await 161 - .and_then(|(_, msg)| match msg { 162 - TransportMessage::Game(game_event) => Some(game_event), 326 + .into_iter() 327 + .filter_map(|(id, msg)| match msg { 328 + TransportMessage::Game(game_event) => Some(*game_event), 329 + TransportMessage::PeerDisconnect => Some(GameEvent::DroppedPlayer(id)), 163 330 _ => None, 164 331 }) 165 332 } 166 333 167 334 async fn send_message(&self, msg: GameEvent) { 168 - let msg = TransportMessage::Game(msg); 169 - self.send_transport_message(None, msg).await; 335 + self.send_transport_message(None, msg.into()).await; 170 336 } 171 337 }
+19
frontend/src/bindings.ts
··· 147 147 if (e instanceof Error) throw e; 148 148 else return { status: "error", error: e as any }; 149 149 } 150 + }, 151 + /** 152 + * (Screen: Menu) Check if a room code is valid to join, use this before starting a game 153 + * for faster error checking. 154 + */ 155 + async checkRoomCode(code: string): Promise<Result<boolean, string>> { 156 + try { 157 + return { status: "ok", data: await TAURI_INVOKE("check_room_code", { code }) }; 158 + } catch (e) { 159 + if (e instanceof Error) throw e; 160 + else return { status: "error", error: e as any }; 161 + } 150 162 } 151 163 }; 152 164 153 165 /** user-defined events **/ 154 166 167 + export const events = __makeEvents__<{ 168 + changeScreen: ChangeScreen; 169 + }>({ 170 + changeScreen: "change-screen" 171 + }); 172 + 155 173 /** user-defined constants **/ 156 174 157 175 /** user-defined types **/ 158 176 159 177 export type AppScreen = "Setup" | "Menu" | "Lobby" | "Game"; 178 + export type ChangeScreen = AppScreen; 160 179 /** 161 180 * Settings for the game, host is the only person able to change these 162 181 */
+2 -2
manhunt-signaling/main.rs
··· 4 4 extract::{Path, ws::Message}, 5 5 http::StatusCode, 6 6 response::IntoResponse, 7 - routing::get, 7 + routing::{get, post}, 8 8 }; 9 9 use futures::StreamExt; 10 10 use log::{debug, error, info, warn}; ··· 309 309 ) 310 310 .route( 311 311 "/mark_started/{id}", 312 - get(move |Path(room_id): Path<String>| async move { 312 + post(move |Path(room_id): Path<String>| async move { 313 313 state2.mark_started(&room_id); 314 314 StatusCode::OK 315 315 }),