···395395 available_powerup: self.available_powerup,
396396 pings: self.pings.clone(),
397397 game_started: self.game_started,
398398+ game_ended: self.game_ended,
398399 last_global_ping: self.last_global_ping,
399400 held_powerup: self.held_powerup,
400401 seekers_started: self.seekers_started,
···428429 pings: HashMap<Uuid, PlayerPing>,
429430 /// When the game was started **in UTC**
430431 game_started: UtcDT,
432432+ /// When the game ended, when this is Option::Some, the game has ended
433433+ game_ended: Option<UtcDT>,
431434 /// The last time all hiders were pinged **in UTC**
432435 last_global_ping: Option<UtcDT>,
433436 /// The [PowerUpType] the local player is holding
+11-16
backend/src/lib.rs
···33mod lobby;
44mod location;
55mod profile;
66+mod server;
67mod transport;
7889use std::{collections::HashMap, sync::Arc, time::Duration};
···1112use history::AppGameHistory;
1213use lobby::{Lobby, LobbyState, StartGameInfo};
1314use location::TauriLocation;
1414-use log::{error, warn};
1515+use log::{error, info, warn, LevelFilter};
1516use profile::PlayerProfile;
1616-use reqwest::StatusCode;
1717use serde::{Deserialize, Serialize};
1818use tauri::{AppHandle, Manager, State};
1919use tauri_specta::{collect_commands, collect_events, Event};
···60606161const GAME_TICK_RATE: Duration = Duration::from_secs(1);
62626363-pub const fn server_url() -> &'static str {
6464- if let Some(url) = option_env!("APP_SERVER_URL") {
6565- url
6666- } else {
6767- "ws://localhost:3536"
6868- }
6969-}
7070-7163/// The app is changing screens, contains the screen it's switching to
7264#[derive(Serialize, Deserialize, Clone, Debug, specta::Type, tauri_specta::Event)]
7365struct ChangeScreen(AppScreen);
···10496 state_updates,
10597 ));
10698 *self = AppState::Game(game.clone(), profiles.clone());
9999+ Self::emit_screen_change(&app, AppScreen::Game);
107100 tokio::spawn(async move {
108101 let res = game.main_loop().await;
109102 let app2 = app.clone();
···212205 let host = join_code.is_none();
213206 let room_code = join_code.unwrap_or_else(generate_join_code);
214207 let lobby = Arc::new(Lobby::new(
215215- server_url(),
216208 &room_code,
217209 host,
218210 profile.clone(),
···228220 let mut state = state_handle.write().await;
229221 match res {
230222 Ok((my_id, start)) => {
223223+ info!("Starting game as {my_id}");
231224 state.start_game(app_game, my_id, start).await;
232225 }
233226 Err(why) => {
···344337#[specta::specta]
345338/// (Screen: Menu) Check if a room code is valid to join, use this before starting a game
346339/// for faster error checking.
347347-async fn check_room_code(code: &str) -> Result<bool, String> {
348348- let url = format!("{}/room_exists/{code}", server_url());
349349- reqwest::get(url)
340340+async fn check_room_code(code: &str) -> Result<bool> {
341341+ server::room_exists(code)
350342 .await
351351- .map(|resp| resp.status() == StatusCode::OK)
352343 .map_err(|err| err.to_string())
353344}
354345···523514524515 tauri::Builder::default()
525516 .plugin(tauri_plugin_notification::init())
526526- .plugin(tauri_plugin_log::Builder::new().build())
517517+ .plugin(
518518+ tauri_plugin_log::Builder::new()
519519+ .level(LevelFilter::Debug)
520520+ .build(),
521521+ )
527522 .plugin(tauri_plugin_opener::init())
528523 .plugin(tauri_plugin_geolocation::init())
529524 .plugin(tauri_plugin_store::Builder::default().build())
+38-33
backend/src/lobby.rs
···1111 game::GameSettings,
1212 prelude::*,
1313 profile::PlayerProfile,
1414- server_url,
1414+ server,
1515 transport::{MatchboxTransport, TransportMessage},
1616};
1717···3939 join_code: String,
4040 /// True represents seeker, false hider
4141 teams: HashMap<Uuid, bool>,
4242+ self_id: Option<Uuid>,
4243 self_seeker: bool,
4444+ is_host: bool,
4345 settings: GameSettings,
4446}
4547···59616062impl Lobby {
6163 pub fn new(
6262- ws_url_base: &str,
6364 join_code: &str,
6465 host: bool,
6566 profile: PlayerProfile,
···6869 ) -> Self {
6970 Self {
7071 app,
7171- transport: Arc::new(MatchboxTransport::new(&format!(
7272- "{ws_url_base}/{join_code}{}",
7373- if host { "?create" } else { "" }
7474- ))),
7272+ transport: Arc::new(MatchboxTransport::new(join_code, host)),
7573 is_host: host,
7674 self_profile: profile,
7775 join_code: join_code.to_string(),
···8078 join_code: join_code.to_string(),
8179 profiles: HashMap::with_capacity(5),
8280 self_seeker: false,
8181+ self_id: None,
8282+ is_host: host,
8383 settings,
8484 }),
8585 }
···108108 pub async fn switch_teams(&self, seeker: bool) {
109109 let mut state = self.state.lock().await;
110110 state.self_seeker = seeker;
111111+ if let Some(id) = state.self_id {
112112+ if let Some(state_seeker) = state.teams.get_mut(&id) {
113113+ *state_seeker = seeker;
114114+ }
115115+ }
111116 drop(state);
112117 self.transport
113118 .send_transport_message(None, LobbyMessage::PlayerSwitch(seeker).into())
···131136 self.transport.send_transport_message(id, msg.into()).await
132137 }
133138134134- async fn singaling_mark_started(&self) -> Result {
135135- let url = format!("{}/mark_started/{}", server_url(), &self.join_code);
136136- let client = reqwest::Client::builder().build()?;
137137- client.post(url).send().await?.error_for_status()?;
138138- Ok(())
139139+ async fn signaling_mark_started(&self) -> Result {
140140+ server::mark_room_started(&self.join_code).await
139141 }
140142141143 /// (Host) Start the game
142144 pub async fn start_game(&self) {
143145 if self.is_host {
144144- if let Some(my_id) = self.transport.get_my_id().await {
145145- let mut state = self.state.lock().await;
146146- let seeker = state.self_seeker;
147147- state.teams.insert(my_id, seeker);
148148- let start_game_info = StartGameInfo {
149149- settings: state.settings.clone(),
150150- initial_caught_state: state.teams.clone(),
151151- };
152152- drop(state);
153153- let msg = LobbyMessage::StartGame(start_game_info);
154154- self.send_transport_message(None, msg).await;
155155- if let Err(why) = self.singaling_mark_started().await {
156156- warn!("Failed to tell signalling server that the match started: {why:?}");
157157- }
158158- self.emit_state_update();
146146+ let state = self.state.lock().await;
147147+ let start_game_info = StartGameInfo {
148148+ settings: state.settings.clone(),
149149+ initial_caught_state: state.teams.clone(),
150150+ };
151151+ drop(state);
152152+ let msg = LobbyMessage::StartGame(start_game_info);
153153+ self.send_transport_message(None, msg).await;
154154+ if let Err(why) = self.signaling_mark_started().await {
155155+ warn!("Failed to tell signalling server that the match started: {why:?}");
159156 }
157157+ self.emit_state_update();
160158 }
161159 }
162160···175173176174 for (peer, msg) in msgs {
177175 match msg {
176176+ TransportMessage::IdAssigned(id) => {
177177+ let mut state = self.state.lock().await;
178178+ state.self_id = Some(id);
179179+ let seeker = state.self_seeker;
180180+ state.teams.insert(id, seeker);
181181+ state.profiles.insert(id, self.self_profile.clone());
182182+ }
178183 TransportMessage::Disconnected => {
179184 break 'lobby Err(anyhow!(
180185 "Transport disconnected before lobby could start game"
···193198 state.settings = game_settings;
194199 }
195200 LobbyMessage::StartGame(start_game_info) => {
196196- break 'lobby Ok((
197197- self.transport
198198- .get_my_id()
199199- .await
200200- .expect("Error getting self ID"),
201201- start_game_info,
202202- ));
201201+ let id = self
202202+ .state
203203+ .lock()
204204+ .await
205205+ .self_id
206206+ .expect("Error getting self ID");
207207+ break 'lobby Ok((id, start_game_info));
203208 }
204209 LobbyMessage::PlayerSwitch(seeker) => {
205210 let mut state = self.state.lock().await;
···11+/// <reference types="vite/client" />
22+13import { defineConfig } from "vite";
24import react from "@vitejs/plugin-react";
55+import path from "path";
3644-// @ts-expect-error process is a nodejs global
57const host = process.env.TAURI_DEV_HOST;
6877-// https://vite.dev/config/
89export default defineConfig(async () => ({
910 plugins: [react()],
1011 clearScreen: false,
···1920 port: 1421
2021 }
2122 : undefined
2323+ },
2424+ resolve: {
2525+ alias: [{ find: "@", replacement: path.resolve(__dirname, "./src") }]
2226 }
2327}));
+4
justfile
···1010dev:
1111 cargo tauri dev
12121313+# Start a webview window *without* running the frontend, only one frontend needs to run at once
1414+dev-window:
1515+ cargo run -p manhunt-app
1616+1317# Format everything
1418fmt:
1519 cargo fmt