···64 Game and lobby logic for the app
65- [manhunt-transport/](https://github.com/Bwc9876/manhunt-app/tree/main/manhunt-transport):
66 Transport (networking) implementation for communication between apps
67-- [backend/](https://github.com/Bwc9876/manhunt-app/tree/main/backend): App
68 backend, Rust side of the Tauri application
69- [frontend/](https://github.com/Bwc9876/manhunt-app/tree/main/frontend): App
70 frontend, Web side of the Tauri application
···83- `just check-frontend`: Check for potential issues on the frontend
84 (only need to run if you edited the frontend)
8586-**Important**: When changing any type in `backend` that derives `specta::Type`,
87you need to run `just export-types` to sync these type bindings to the frontend.
88Otherwise the TypeScript definitions will not match the ones that the backend expects.
89
···64 Game and lobby logic for the app
65- [manhunt-transport/](https://github.com/Bwc9876/manhunt-app/tree/main/manhunt-transport):
66 Transport (networking) implementation for communication between apps
67+- [manhunt-app/](https://github.com/Bwc9876/manhunt-app/tree/main/manhunt-app): App
68 backend, Rust side of the Tauri application
69- [frontend/](https://github.com/Bwc9876/manhunt-app/tree/main/frontend): App
70 frontend, Web side of the Tauri application
···83- `just check-frontend`: Check for potential issues on the frontend
84 (only need to run if you edited the frontend)
8586+**Important**: When changing any type in a rust file that derives `specta::Type`,
87you need to run `just export-types` to sync these type bindings to the frontend.
88Otherwise the TypeScript definitions will not match the ones that the backend expects.
89
···37 npm run lint
3839# Export types from the backend to TypeScript bindings
40-[working-directory('backend')]
41export-types:
42- cargo run --bin export-types ../frontend/src/bindings.ts
43- prettier --write ../frontend/src/bindings.ts --config ../.prettierrc.yaml
4445# Start the signaling server on localhost:3536
46[working-directory('manhunt-signaling')]
···37 npm run lint
3839# Export types from the backend to TypeScript bindings
040export-types:
41+ cargo run --bin export-types frontend/src/bindings.ts
42+ prettier --write frontend/src/bindings.ts --config .prettierrc.yaml
4344# Start the signaling server on localhost:3536
45[working-directory('manhunt-signaling')]
···1+mod history;
2+mod location;
3+mod profiles;
4+mod state;
5+6+use std::collections::HashMap;
7+8+use log::LevelFilter;
9+use manhunt_logic::{GameSettings, GameUiState, LobbyState, PlayerProfile, UtcDT};
10+use manhunt_transport::room_exists;
11+use tauri::{AppHandle, Manager, State};
12+use tauri_specta::{ErrorHandlingMode, collect_commands, collect_events};
13+use tokio::sync::RwLock;
14+use uuid::Uuid;
15+16+use std::result::Result as StdResult;
17+18+use crate::{
19+ history::AppGameHistory,
20+ profiles::{read_profile_from_store, write_profile_to_store},
21+ state::{AppScreen, AppState, AppStateHandle, ChangeScreen, GameStateUpdate, LobbyStateUpdate},
22+};
23+24+type Result<T = (), E = String> = StdResult<T, E>;
25+26+// == GENERAL / FLOW COMMANDS ==
27+28+#[tauri::command]
29+#[specta::specta]
30+/// Get the screen the app should currently be on, returns [AppScreen]
31+async fn get_current_screen(state: State<'_, AppStateHandle>) -> Result<AppScreen> {
32+ let state = state.read().await;
33+ Ok(match &*state {
34+ AppState::Setup => AppScreen::Setup,
35+ AppState::Menu(_player_profile) => AppScreen::Menu,
36+ AppState::Lobby(_lobby) => AppScreen::Lobby,
37+ AppState::Game(_game, _profiles) => AppScreen::Game,
38+ AppState::Replay(_) => AppScreen::Replay,
39+ })
40+}
41+42+#[tauri::command]
43+#[specta::specta]
44+/// Quit a running game or leave a lobby
45+async fn quit_to_menu(app: AppHandle, state: State<'_, AppStateHandle>) -> Result {
46+ let mut state = state.write().await;
47+ state.quit_to_menu(app).await;
48+ Ok(())
49+}
50+51+// == AppState::Setup COMMANDS
52+53+#[tauri::command]
54+#[specta::specta]
55+/// (Screen: Setup) Complete user setup and go to the menu screen
56+async fn complete_setup(
57+ profile: PlayerProfile,
58+ app: AppHandle,
59+ state: State<'_, AppStateHandle>,
60+) -> Result {
61+ state.write().await.complete_setup(&app, profile)
62+}
63+64+// == AppState::Menu COMMANDS ==
65+66+#[tauri::command]
67+#[specta::specta]
68+/// (Screen: Menu) Get the user's player profile
69+async fn get_profile(state: State<'_, AppStateHandle>) -> Result<PlayerProfile> {
70+ let state = state.read().await;
71+ let profile = state.get_menu()?;
72+ Ok(profile.clone())
73+}
74+75+#[tauri::command]
76+#[specta::specta]
77+/// (Screen: Menu) Get a list of all previously played games, returns of list of DateTimes that represent when
78+/// each game started, use this as a key
79+fn list_game_histories(app: AppHandle) -> Result<Vec<UtcDT>> {
80+ AppGameHistory::ls_histories(&app)
81+ .map_err(|err| err.context("Failed to get game histories").to_string())
82+}
83+84+#[tauri::command]
85+#[specta::specta]
86+/// (Screen: Menu) Go to the game replay screen to replay the game history specified by id
87+async fn replay_game(id: UtcDT, app: AppHandle, state: State<'_, AppStateHandle>) -> Result {
88+ state.write().await.replay_game(&app, id)
89+}
90+91+#[tauri::command]
92+#[specta::specta]
93+/// (Screen: Menu) Check if a room code is valid to join, use this before starting a game
94+/// for faster error checking.
95+async fn check_room_code(code: &str) -> Result<bool> {
96+ room_exists(code).await.map_err(|err| err.to_string())
97+}
98+99+#[tauri::command]
100+#[specta::specta]
101+/// (Screen: Menu) Update the player's profile and persist it
102+async fn update_profile(
103+ new_profile: PlayerProfile,
104+ app: AppHandle,
105+ state: State<'_, AppStateHandle>,
106+) -> Result {
107+ write_profile_to_store(&app, new_profile.clone());
108+ let mut state = state.write().await;
109+ let profile = state.get_menu_mut()?;
110+ *profile = new_profile;
111+ Ok(())
112+}
113+114+#[tauri::command]
115+#[specta::specta]
116+/// (Screen: Menu) Start/Join a new lobby, set `join_code` to `null` to be host,
117+/// set it to a join code to be a client. This triggers a screen change to [AppScreen::Lobby]
118+async fn start_lobby(
119+ app: AppHandle,
120+ join_code: Option<String>,
121+ settings: GameSettings,
122+ state: State<'_, AppStateHandle>,
123+) -> Result {
124+ let mut state = state.write().await;
125+ state.start_lobby(join_code, app, settings).await;
126+ Ok(())
127+}
128+129+// AppState::Lobby COMMANDS
130+131+#[tauri::command]
132+#[specta::specta]
133+/// (Screen: Lobby) Get the current state of the lobby, call after receiving an update event
134+async fn get_lobby_state(state: State<'_, AppStateHandle>) -> Result<LobbyState> {
135+ let lobby = state.read().await.get_lobby()?;
136+ Ok(lobby.clone_state().await)
137+}
138+139+#[tauri::command]
140+#[specta::specta]
141+/// (Screen: Lobby) Switch teams between seekers and hiders, returns the new [LobbyState]
142+async fn switch_teams(seeker: bool, state: State<'_, AppStateHandle>) -> Result {
143+ let lobby = state.read().await.get_lobby()?;
144+ lobby.switch_teams(seeker).await;
145+ Ok(())
146+}
147+148+#[tauri::command]
149+#[specta::specta]
150+/// (Screen: Lobby) HOST ONLY: Push new settings to everyone, does nothing on clients. Returns the
151+/// new lobby state
152+async fn host_update_settings(settings: GameSettings, state: State<'_, AppStateHandle>) -> Result {
153+ let lobby = state.read().await.get_lobby()?;
154+ lobby.update_settings(settings).await;
155+ Ok(())
156+}
157+158+#[tauri::command]
159+#[specta::specta]
160+/// (Screen: Lobby) HOST ONLY: Start the game, stops anyone else from joining and switched screen
161+/// to AppScreen::Game.
162+async fn host_start_game(state: State<'_, AppStateHandle>) -> Result {
163+ state.read().await.get_lobby()?.start_game().await;
164+ Ok(())
165+}
166+167+// AppScreen::Game COMMANDS
168+169+#[tauri::command]
170+#[specta::specta]
171+/// (Screen: Game) Get all player profiles with display names and profile pictures for this game.
172+/// This value will never change and is fairly expensive to clone, so please minimize calls to
173+/// this command.
174+async fn get_profiles(state: State<'_, AppStateHandle>) -> Result<HashMap<Uuid, PlayerProfile>> {
175+ state.read().await.get_profiles().cloned()
176+}
177+178+#[tauri::command]
179+#[specta::specta]
180+/// (Screen: Game) Get the current settings for this game.
181+async fn get_game_settings(state: State<'_, AppStateHandle>) -> Result<GameSettings> {
182+ Ok(state.read().await.get_game()?.clone_settings().await)
183+}
184+185+#[tauri::command]
186+#[specta::specta]
187+/// (Screen: Game) Get the current state of the game.
188+async fn get_game_state(state: State<'_, AppStateHandle>) -> Result<GameUiState> {
189+ Ok(state.read().await.get_game()?.get_ui_state().await)
190+}
191+192+#[tauri::command]
193+#[specta::specta]
194+/// (Screen: Game) Mark this player as caught, this player will become a seeker. Returns the new game state
195+async fn mark_caught(state: State<'_, AppStateHandle>) -> Result {
196+ let game = state.read().await.get_game()?;
197+ game.mark_caught().await;
198+ Ok(())
199+}
200+201+#[tauri::command]
202+#[specta::specta]
203+/// (Screen: Game) Grab a powerup on the map, this should be called when the user is *in range* of
204+/// the powerup. Returns the new game state after rolling for the powerup
205+async fn grab_powerup(state: State<'_, AppStateHandle>) -> Result {
206+ let game = state.read().await.get_game()?;
207+ game.get_powerup().await;
208+ Ok(())
209+}
210+211+#[tauri::command]
212+#[specta::specta]
213+/// (Screen: Game) Use the currently held powerup in the player's held_powerup. Does nothing if the
214+/// player has none. Returns the updated game state
215+async fn activate_powerup(state: State<'_, AppStateHandle>) -> Result {
216+ let game = state.read().await.get_game()?;
217+ game.use_powerup().await;
218+ Ok(())
219+}
220+221+// AppState::Replay COMMANDS
222+223+#[tauri::command]
224+#[specta::specta]
225+/// (Screen: Replay) Get the game history that's currently being replayed. Try to limit calls to
226+/// this
227+async fn get_current_replay_history(state: State<'_, AppStateHandle>) -> Result<AppGameHistory> {
228+ state.read().await.get_replay()
229+}
230+231+pub fn mk_specta() -> tauri_specta::Builder {
232+ tauri_specta::Builder::<tauri::Wry>::new()
233+ .error_handling(ErrorHandlingMode::Throw)
234+ .commands(collect_commands![
235+ start_lobby,
236+ get_profile,
237+ quit_to_menu,
238+ get_current_screen,
239+ update_profile,
240+ get_lobby_state,
241+ host_update_settings,
242+ switch_teams,
243+ host_start_game,
244+ mark_caught,
245+ grab_powerup,
246+ activate_powerup,
247+ check_room_code,
248+ get_profiles,
249+ replay_game,
250+ list_game_histories,
251+ get_current_replay_history,
252+ get_game_settings,
253+ get_game_state,
254+ complete_setup,
255+ ])
256+ .events(collect_events![
257+ ChangeScreen,
258+ GameStateUpdate,
259+ LobbyStateUpdate
260+ ])
261+}
262+263+#[cfg_attr(mobile, tauri::mobile_entry_point)]
264+pub fn run() {
265+ let state = RwLock::new(AppState::Setup);
266+267+ let builder = mk_specta();
268+269+ tauri::Builder::default()
270+ .plugin(tauri_plugin_dialog::init())
271+ .plugin(tauri_plugin_notification::init())
272+ .plugin(
273+ tauri_plugin_log::Builder::new()
274+ .level(LevelFilter::Debug)
275+ .build(),
276+ )
277+ .plugin(tauri_plugin_opener::init())
278+ .plugin(tauri_plugin_geolocation::init())
279+ .plugin(tauri_plugin_store::Builder::default().build())
280+ .invoke_handler(builder.invoke_handler())
281+ .manage(state)
282+ .setup(move |app| {
283+ builder.mount_events(app);
284+285+ let handle = app.handle().clone();
286+ tauri::async_runtime::spawn(async move {
287+ if let Some(profile) = read_profile_from_store(&handle) {
288+ let state_handle = handle.state::<AppStateHandle>();
289+ let mut state = state_handle.write().await;
290+ *state = AppState::Menu(profile);
291+ }
292+ });
293+ Ok(())
294+ })
295+ .run(tauri::generate_context!())
296+ .expect("error while running tauri application");
297+}