diff --git a/TODO.md b/TODO.md index 13ba85b..d5bc337 100644 --- a/TODO.md +++ b/TODO.md @@ -20,5 +20,6 @@ - [x] Signaling: All of it - [x] Backend : Better transport error handling - [ ] Backend : Abstract lobby? Separate crate? +- [x] Transport : Handle transport cancellation better - [x] Backend : Add checks for when the `powerup_locations` field is an empty array in settings - [ ] Backend : More tests diff --git a/backend/src/game/mod.rs b/backend/src/game/mod.rs index 10743e6..f3462b0 100644 --- a/backend/src/game/mod.rs +++ b/backend/src/game/mod.rs @@ -117,7 +117,7 @@ impl Game { } } - async fn consume_event(&self, state: &mut GameState, event: GameEvent) -> Result { + async fn consume_event(&self, state: &mut GameState, event: GameEvent) -> Result { if !state.game_ended() { state.event_history.push((Utc::now(), event.clone())); } @@ -126,7 +126,7 @@ impl Game { GameEvent::Ping(player_ping) => state.add_ping(player_ping), GameEvent::ForcePing(target, display) => { if target != state.id { - return Ok(()); + return Ok(false); } let ping = if let Some(display) = display { @@ -149,7 +149,7 @@ impl Game { state.remove_player(id); } GameEvent::TransportDisconnect => { - bail!("Transport disconnected"); + return Ok(true); } GameEvent::TransportError(err) => { bail!("Transport error: {err}"); @@ -161,7 +161,7 @@ impl Game { self.state_update_sender.send_update(); - Ok(()) + Ok(false) } /// Perform a tick for a specific moment in time @@ -253,7 +253,7 @@ impl Game { } /// Main loop of the game, handles ticking and receiving messages from [Transport]. - pub async fn main_loop(&self) -> Result { + pub async fn main_loop(&self) -> Result> { let mut interval = tokio::time::interval(self.interval); interval.set_missed_tick_behavior(MissedTickBehavior::Delay); @@ -265,8 +265,13 @@ impl Game { events = self.transport.receive_messages() => { let mut state = self.state.write().await; for event in events { - if let Err(why) = self.consume_event(&mut state, event).await { - break 'game Err(why); + match self.consume_event(&mut state, event).await { + Ok(should_break) => { + if should_break { + break 'game Ok(None); + } + } + Err(why) => { break 'game Err(why); } } } } @@ -277,7 +282,7 @@ impl Game { if should_break { let history = state.as_game_history(); - break Ok(history); + break Ok(Some(history)); } } } diff --git a/backend/src/lib.rs b/backend/src/lib.rs index 42075d3..e006856 100644 --- a/backend/src/lib.rs +++ b/backend/src/lib.rs @@ -17,7 +17,7 @@ use profile::PlayerProfile; use serde::{Deserialize, Serialize}; use tauri::{AppHandle, Manager, State}; use tauri_plugin_dialog::{DialogExt, MessageDialogKind}; -use tauri_specta::{collect_commands, collect_events, Event}; +use tauri_specta::{collect_commands, collect_events, ErrorHandlingMode, Event}; use tokio::sync::RwLock; use transport::MatchboxTransport; use uuid::Uuid; @@ -104,7 +104,7 @@ impl AppState { let state_handle = app.state::(); let mut state = state_handle.write().await; match res { - Ok(history) => { + Ok(Some(history)) => { let history = AppGameHistory::new(history, profiles); if let Err(why) = history.save_history(&app2) { error!("Failed to save game history: {why:?}"); @@ -115,9 +115,15 @@ impl AppState { } state.quit_to_menu(app2); } + Ok(None) => { + info!("User quit game"); + } Err(why) => { error!("Game Error: {why:?}"); - app2.dialog().message("There was a connection error in the game, you have been disconnected").kind(MessageDialogKind::Error).show(|_| {}); + app2.dialog() + .message(format!("Connection Error: {why}")) + .kind(MessageDialogKind::Error) + .show(|_| {}); state.quit_to_menu(app2); } } @@ -225,15 +231,18 @@ impl AppState { let state_handle = app2.state::(); let mut state = state_handle.write().await; match res { - Ok((my_id, start)) => { + Ok(Some((my_id, start))) => { info!("Starting game as {my_id}"); state.start_game(app_game, my_id, start).await; } + Ok(None) => { + info!("User quit lobby"); + } Err(why) => { - error!("Lobby Error: {why:?}"); + error!("Lobby Error: {why}"); app_game .dialog() - .message("Error joining the lobby") + .message(format!("Error joining the lobby: {why}")) .kind(MessageDialogKind::Error) .show(|_| {}); state.quit_to_menu(app_game); @@ -470,7 +479,7 @@ async fn grab_powerup(state: State<'_, AppStateHandle>) -> Result { #[specta::specta] /// (Screen: Game) Use the currently held powerup in the player's held_powerup. Does nothing if the /// player has none. Returns the updated game state -async fn use_powerup(state: State<'_, AppStateHandle>) -> Result { +async fn activate_powerup(state: State<'_, AppStateHandle>) -> Result { let game = state.read().await.get_game()?; game.use_powerup().await; Ok(()) @@ -488,6 +497,7 @@ async fn get_current_replay_history(state: State<'_, AppStateHandle>) -> Result< pub fn mk_specta() -> tauri_specta::Builder { tauri_specta::Builder::::new() + .error_handling(ErrorHandlingMode::Throw) .commands(collect_commands![ start_lobby, get_profile, @@ -500,7 +510,7 @@ pub fn mk_specta() -> tauri_specta::Builder { host_start_game, mark_caught, grab_powerup, - use_powerup, + activate_powerup, check_room_code, get_profiles, replay_game, diff --git a/backend/src/lobby.rs b/backend/src/lobby.rs index eee0de2..fc6c96a 100644 --- a/backend/src/lobby.rs +++ b/backend/src/lobby.rs @@ -162,7 +162,7 @@ impl Lobby { self.transport.cancel(); } - pub async fn open(&self) -> Result<(Uuid, StartGameInfo)> { + pub async fn open(&self) -> Result> { let transport_inner = self.transport.clone(); tokio::spawn(async move { transport_inner.transport_loop().await }); @@ -181,9 +181,7 @@ impl Lobby { state.profiles.insert(id, self.self_profile.clone()); } TransportMessage::Disconnected => { - break 'lobby Err(anyhow!( - "Transport disconnected unexpectedly before lobby could start game" - )); + break 'lobby Ok(None); } TransportMessage::Error(why) => { break 'lobby Err(anyhow!("Transport error: {why}")); @@ -207,7 +205,7 @@ impl Lobby { .await .self_id .expect("Error getting self ID"); - break 'lobby Ok((id, start_game_info)); + break 'lobby Ok(Some((id, start_game_info))); } LobbyMessage::PlayerSwitch(seeker) => { let mut state = self.state.lock().await; diff --git a/backend/src/transport.rs b/backend/src/transport.rs index ea08f23..c84b3f5 100644 --- a/backend/src/transport.rs +++ b/backend/src/transport.rs @@ -6,7 +6,7 @@ use std::{ use anyhow::Context; use futures::FutureExt; use log::error; -use matchbox_socket::{PeerId, PeerState, WebRtcSocket}; +use matchbox_socket::{Error as SocketError, PeerId, PeerState, WebRtcSocket}; use serde::{Deserialize, Serialize}; use tokio::sync::{Mutex, RwLock}; use tokio_util::sync::CancellationToken; @@ -206,7 +206,26 @@ impl MatchboxTransport { pub async fn transport_loop(&self) { let (mut socket, loop_fut) = WebRtcSocket::new_reliable(&self.ws_url); - let loop_fut = loop_fut.fuse(); + let loop_fut = async { + let msg = match loop_fut.await { + Ok(_) => TransportMessage::Disconnected, + Err(e) => { + let msg = match e { + SocketError::ConnectionFailed(e) => { + format!("Failed to connect to server: {e}") + } + SocketError::Disconnected(e) => { + format!("Disconnected from server, network error or kick: {e}") + } + }; + TransportMessage::Error(msg) + } + }; + self.push_incoming(self.my_id.read().await.unwrap_or_default(), msg) + .await; + } + .fuse(); + tokio::pin!(loop_fut); let mut all_peers = HashSet::::with_capacity(20); @@ -310,29 +329,28 @@ impl MatchboxTransport { let mut buffer = Vec::with_capacity(30); tokio::select! { - _ = self.cancel_token.cancelled() => { - socket.close(); + // Break if cancelled externally + break; } - _ = timer.tick() => { - // Transport Tick + _ = &mut loop_fut => { + // Break if disconnected + break; } - _ = outgoing_rx.recv_many(&mut buffer, 30) => { + // Rerun every tick + _ = timer.tick() => {} + _ = outgoing_rx.recv_many(&mut buffer, 30) => { + // Handle sending new messages self.handle_send(&mut socket, &all_peers, &mut buffer).await; } - - res = &mut loop_fut => { - // Break on disconnect - if let Err(why) = res { - self.push_incoming(my_id.unwrap_or_default(), TransportMessage::Error(why.to_string())).await; - } - break; - } } } + + drop(socket); + loop_fut.await } } diff --git a/frontend/src/bindings.ts b/frontend/src/bindings.ts index 635a9aa..7bfc52a 100644 --- a/frontend/src/bindings.ts +++ b/frontend/src/bindings.ts @@ -9,237 +9,131 @@ export const commands = { * (Screen: Menu) Start/Join a new lobby, set `join_code` to `null` to be host, * set it to a join code to be a client. This triggers a screen change to [AppScreen::Lobby] */ - async startLobby( - joinCode: string | null, - settings: GameSettings - ): Promise> { - try { - return { - status: "ok", - data: await TAURI_INVOKE("start_lobby", { joinCode, settings }) - }; - } catch (e) { - if (e instanceof Error) throw e; - else return { status: "error", error: e as any }; - } + async startLobby(joinCode: string | null, settings: GameSettings): Promise { + return await TAURI_INVOKE("start_lobby", { joinCode, settings }); }, /** * (Screen: Menu) Get the user's player profile */ - async getProfile(): Promise> { - try { - return { status: "ok", data: await TAURI_INVOKE("get_profile") }; - } catch (e) { - if (e instanceof Error) throw e; - else return { status: "error", error: e as any }; - } + async getProfile(): Promise { + return await TAURI_INVOKE("get_profile"); }, /** * Quit a running game or leave a lobby */ - async quitToMenu(): Promise> { - try { - return { status: "ok", data: await TAURI_INVOKE("quit_to_menu") }; - } catch (e) { - if (e instanceof Error) throw e; - else return { status: "error", error: e as any }; - } + async quitToMenu(): Promise { + return await TAURI_INVOKE("quit_to_menu"); }, /** * Get the screen the app should currently be on, returns [AppScreen] */ - async getCurrentScreen(): Promise> { - try { - return { status: "ok", data: await TAURI_INVOKE("get_current_screen") }; - } catch (e) { - if (e instanceof Error) throw e; - else return { status: "error", error: e as any }; - } + async getCurrentScreen(): Promise { + return await TAURI_INVOKE("get_current_screen"); }, /** * (Screen: Menu) Update the player's profile and persist it */ - async updateProfile(newProfile: PlayerProfile): Promise> { - try { - return { status: "ok", data: await TAURI_INVOKE("update_profile", { newProfile }) }; - } catch (e) { - if (e instanceof Error) throw e; - else return { status: "error", error: e as any }; - } + async updateProfile(newProfile: PlayerProfile): Promise { + return await TAURI_INVOKE("update_profile", { newProfile }); }, /** * (Screen: Lobby) Get the current state of the lobby, call after receiving an update event */ - async getLobbyState(): Promise> { - try { - return { status: "ok", data: await TAURI_INVOKE("get_lobby_state") }; - } catch (e) { - if (e instanceof Error) throw e; - else return { status: "error", error: e as any }; - } + async getLobbyState(): Promise { + return await TAURI_INVOKE("get_lobby_state"); }, /** * (Screen: Lobby) HOST ONLY: Push new settings to everyone, does nothing on clients. Returns the * new lobby state */ - async hostUpdateSettings(settings: GameSettings): Promise> { - try { - return { status: "ok", data: await TAURI_INVOKE("host_update_settings", { settings }) }; - } catch (e) { - if (e instanceof Error) throw e; - else return { status: "error", error: e as any }; - } + async hostUpdateSettings(settings: GameSettings): Promise { + return await TAURI_INVOKE("host_update_settings", { settings }); }, /** * (Screen: Lobby) Switch teams between seekers and hiders, returns the new [LobbyState] */ - async switchTeams(seeker: boolean): Promise> { - try { - return { status: "ok", data: await TAURI_INVOKE("switch_teams", { seeker }) }; - } catch (e) { - if (e instanceof Error) throw e; - else return { status: "error", error: e as any }; - } + async switchTeams(seeker: boolean): Promise { + return await TAURI_INVOKE("switch_teams", { seeker }); }, /** * (Screen: Lobby) HOST ONLY: Start the game, stops anyone else from joining and switched screen * to AppScreen::Game. */ - async hostStartGame(): Promise> { - try { - return { status: "ok", data: await TAURI_INVOKE("host_start_game") }; - } catch (e) { - if (e instanceof Error) throw e; - else return { status: "error", error: e as any }; - } + async hostStartGame(): Promise { + return await TAURI_INVOKE("host_start_game"); }, /** * (Screen: Game) Mark this player as caught, this player will become a seeker. Returns the new game state */ - async markCaught(): Promise> { - try { - return { status: "ok", data: await TAURI_INVOKE("mark_caught") }; - } catch (e) { - if (e instanceof Error) throw e; - else return { status: "error", error: e as any }; - } + async markCaught(): Promise { + return await TAURI_INVOKE("mark_caught"); }, /** * (Screen: Game) Grab a powerup on the map, this should be called when the user is *in range* of * the powerup. Returns the new game state after rolling for the powerup */ - async grabPowerup(): Promise> { - try { - return { status: "ok", data: await TAURI_INVOKE("grab_powerup") }; - } catch (e) { - if (e instanceof Error) throw e; - else return { status: "error", error: e as any }; - } + async grabPowerup(): Promise { + return await TAURI_INVOKE("grab_powerup"); }, /** * (Screen: Game) Use the currently held powerup in the player's held_powerup. Does nothing if the * player has none. Returns the updated game state */ - async usePowerup(): Promise> { - try { - return { status: "ok", data: await TAURI_INVOKE("use_powerup") }; - } catch (e) { - if (e instanceof Error) throw e; - else return { status: "error", error: e as any }; - } + async activatePowerup(): Promise { + return await TAURI_INVOKE("activate_powerup"); }, /** * (Screen: Menu) Check if a room code is valid to join, use this before starting a game * for faster error checking. */ - async checkRoomCode(code: string): Promise> { - try { - return { status: "ok", data: await TAURI_INVOKE("check_room_code", { code }) }; - } catch (e) { - if (e instanceof Error) throw e; - else return { status: "error", error: e as any }; - } + async checkRoomCode(code: string): Promise { + return await TAURI_INVOKE("check_room_code", { code }); }, /** * (Screen: Game) Get all player profiles with display names and profile pictures for this game. * This value will never change and is fairly expensive to clone, so please minimize calls to * this command. */ - async getProfiles(): Promise, string>> { - try { - return { status: "ok", data: await TAURI_INVOKE("get_profiles") }; - } catch (e) { - if (e instanceof Error) throw e; - else return { status: "error", error: e as any }; - } + async getProfiles(): Promise> { + return await TAURI_INVOKE("get_profiles"); }, /** * (Screen: Menu) Go to the game replay screen to replay the game history specified by id */ - async replayGame(id: string): Promise> { - try { - return { status: "ok", data: await TAURI_INVOKE("replay_game", { id }) }; - } catch (e) { - if (e instanceof Error) throw e; - else return { status: "error", error: e as any }; - } + async replayGame(id: string): Promise { + return await TAURI_INVOKE("replay_game", { id }); }, /** * (Screen: Menu) Get a list of all previously played games, returns of list of DateTimes that represent when * each game started, use this as a key */ - async listGameHistories(): Promise> { - try { - return { status: "ok", data: await TAURI_INVOKE("list_game_histories") }; - } catch (e) { - if (e instanceof Error) throw e; - else return { status: "error", error: e as any }; - } + async listGameHistories(): Promise { + return await TAURI_INVOKE("list_game_histories"); }, /** * (Screen: Replay) Get the game history that's currently being replayed. Try to limit calls to * this */ - async getCurrentReplayHistory(): Promise> { - try { - return { status: "ok", data: await TAURI_INVOKE("get_current_replay_history") }; - } catch (e) { - if (e instanceof Error) throw e; - else return { status: "error", error: e as any }; - } + async getCurrentReplayHistory(): Promise { + return await TAURI_INVOKE("get_current_replay_history"); }, /** * (Screen: Game) Get the current settings for this game. */ - async getGameSettings(): Promise> { - try { - return { status: "ok", data: await TAURI_INVOKE("get_game_settings") }; - } catch (e) { - if (e instanceof Error) throw e; - else return { status: "error", error: e as any }; - } + async getGameSettings(): Promise { + return await TAURI_INVOKE("get_game_settings"); }, /** * (Screen: Game) Get the current state of the game. */ - async getGameState(): Promise> { - try { - return { status: "ok", data: await TAURI_INVOKE("get_game_state") }; - } catch (e) { - if (e instanceof Error) throw e; - else return { status: "error", error: e as any }; - } + async getGameState(): Promise { + return await TAURI_INVOKE("get_game_state"); }, /** * (Screen: Setup) Complete user setup and go to the menu screen */ - async completeSetup(profile: PlayerProfile): Promise> { - try { - return { status: "ok", data: await TAURI_INVOKE("complete_setup", { profile }) }; - } catch (e) { - if (e instanceof Error) throw e; - else return { status: "error", error: e as any }; - } + async completeSetup(profile: PlayerProfile): Promise { + return await TAURI_INVOKE("complete_setup", { profile }); } }; diff --git a/frontend/src/components/App.tsx b/frontend/src/components/App.tsx index 4c7f653..20c5d0f 100644 --- a/frontend/src/components/App.tsx +++ b/frontend/src/components/App.tsx @@ -1,8 +1,7 @@ import React from "react"; import useSWR from "swr"; import { AppScreen, commands } from "@/bindings"; -import { useTauriEvent } from "@/lib/hooks"; -import { unwrapResult } from "@/lib/result"; +import { useTauriEvent, sharedSwrConfig } from "@/lib/hooks"; import SetupScreen from "./SetupScreen"; import MenuScreen from "./MenuScreen"; import LobbyScreen from "./LobbyScreen"; @@ -26,10 +25,8 @@ function ScreenRouter({ screen }: { screen: AppScreen }) { export default function App() { const { data: screen, mutate } = useSWR( "fetch-screen", - async () => { - return unwrapResult(await commands.getCurrentScreen()); - }, - { suspense: true, dedupingInterval: 100 } + commands.getCurrentScreen, + sharedSwrConfig ); useTauriEvent("changeScreen", (newScreen) => { diff --git a/frontend/src/components/GameScreen.tsx b/frontend/src/components/GameScreen.tsx index 9477eb9..2640666 100644 --- a/frontend/src/components/GameScreen.tsx +++ b/frontend/src/components/GameScreen.tsx @@ -1,20 +1,15 @@ import React from "react"; import { commands } from "@/bindings"; -import { useTauriEvent } from "@/lib/hooks"; +import { sharedSwrConfig, useTauriEvent } from "@/lib/hooks"; import useSWR from "swr"; -import { unwrapResult } from "@/lib/result"; export default function GameScreen() { - const profiles = unwrapResult(React.use(commands.getProfiles())); + const { data: profiles } = useSWR("game-get-profiles", commands.getProfiles); + const { data: gameState, mutate } = useSWR( "fetch-game-state", - async () => { - return unwrapResult(await commands.getGameState()); - }, - { - suspense: true, - dedupingInterval: 100 - } + commands.getGameState, + sharedSwrConfig ); useTauriEvent("gameStateUpdate", () => { @@ -25,24 +20,24 @@ export default function GameScreen() { const markCaught = async () => { if (!isSeeker) { - unwrapResult(await commands.markCaught()); + await commands.markCaught(); } }; const grabPowerup = async () => { if (gameState.available_powerup !== null) { - unwrapResult(await commands.grabPowerup()); + await commands.grabPowerup(); } }; - const usePowerup = async () => { + const activatePowerup = async () => { if (gameState.held_powerup !== null && gameState.held_powerup !== "PingSeeker") { - unwrapResult(await commands.usePowerup()); + await commands.activatePowerup(); } }; const quitToMenu = async () => { - unwrapResult(await commands.quitToMenu()); + await commands.quitToMenu(); }; if (gameState.game_ended) { @@ -56,7 +51,7 @@ export default function GameScreen() { {Object.keys(gameState.caught_state) .filter((k) => !gameState.caught_state[k]) .map((key) => ( -
  • {profiles[key]?.display_name ?? key}
  • +
  • {profiles?.[key]?.display_name ?? key}
  • ))} {!isSeeker && }

    Pings

    @@ -67,7 +62,8 @@ export default function GameScreen() { .filter(([key, v]) => key && v !== undefined) .map(([k, v]) => (
  • - {profiles[v!.display_player]?.display_name ?? v!.display_player} + {profiles?.[v!.display_player]?.display_name ?? + v!.display_player} : {v && JSON.stringify(v.loc)}
  • ))} @@ -90,7 +86,7 @@ export default function GameScreen() { Held Powerup: {gameState.held_powerup} {(gameState.held_powerup === "PingSeeker" && ( (Will be used next ping) - )) || } + )) || }

    )}

    Quit

    diff --git a/frontend/src/components/LobbyScreen.tsx b/frontend/src/components/LobbyScreen.tsx index 9309fc4..6a568c6 100644 --- a/frontend/src/components/LobbyScreen.tsx +++ b/frontend/src/components/LobbyScreen.tsx @@ -1,19 +1,13 @@ import React from "react"; import { commands } from "@/bindings"; -import { useTauriEvent } from "@/lib/hooks"; +import { sharedSwrConfig, useTauriEvent } from "@/lib/hooks"; import useSWR from "swr"; -import { unwrapResult } from "@/lib/result"; export default function LobbyScreen() { const { data: lobbyState, mutate } = useSWR( "fetch-lobby-state", - async () => { - return unwrapResult(await commands.getLobbyState()); - }, - { - suspense: true, - dedupingInterval: 100 - } + commands.getLobbyState, + sharedSwrConfig ); useTauriEvent("lobbyStateUpdate", () => { @@ -21,15 +15,15 @@ export default function LobbyScreen() { }); const setSeeker = async (seeker: boolean) => { - unwrapResult(await commands.switchTeams(seeker)); + await commands.switchTeams(seeker); }; const startGame = async () => { - unwrapResult(await commands.hostStartGame()); + await commands.hostStartGame(); }; const quit = async () => { - unwrapResult(await commands.quitToMenu()); + await commands.quitToMenu(); }; if (lobbyState.self_id === null) { diff --git a/frontend/src/components/MenuScreen.tsx b/frontend/src/components/MenuScreen.tsx index 0c1438d..5e1156f 100644 --- a/frontend/src/components/MenuScreen.tsx +++ b/frontend/src/components/MenuScreen.tsx @@ -1,6 +1,7 @@ import { commands, GameSettings } from "@/bindings"; -import { unwrapResult } from "@/lib/result"; +import { sharedSwrConfig } from "@/lib/hooks"; import React from "react"; +import useSWR from "swr"; // Temp settings for now. const settings: GameSettings = { @@ -20,23 +21,25 @@ const settings: GameSettings = { ] }; -function MainMenu({ - profilePromise, - historyPromise -}: { - profilePromise: ReturnType; - historyPromise: ReturnType; -}) { - const initialProfile = unwrapResult(React.use(profilePromise)); - const gameHistory = unwrapResult(React.use(historyPromise)); - const [profile, setProfile] = React.useState(initialProfile); - const [newName, setName] = React.useState(initialProfile.display_name); +export default function MenuScreen() { const [roomCode, setRoomCode] = React.useState(""); + const [newName, setName] = React.useState(""); + + const { data: profile, mutate: setProfile } = useSWR( + "fetch-profile", + commands.getProfile, + sharedSwrConfig + ); + const { data: gameHistory } = useSWR( + "list-game-history", + commands.listGameHistories, + sharedSwrConfig + ); const onStartGame = async (code: string | null) => { if (code) { try { - const validCode = unwrapResult(await commands.checkRoomCode(code)); + const validCode = await commands.checkRoomCode(code); if (!validCode) { window.alert("Invalid Join Code"); return; @@ -50,10 +53,8 @@ function MainMenu({ }; const onSaveProfile = async () => { - unwrapResult(await commands.updateProfile({ ...profile, display_name: newName })); - setProfile((p) => { - return { ...p, display_name: newName }; - }); + await commands.updateProfile({ ...profile, display_name: newName }); + setProfile({ ...profile, display_name: newName }); }; return ( @@ -77,7 +78,11 @@ function MainMenu({

    Edit Profile

    - setName(e.target.value)} /> + setName(e.target.value)} + />

    Previous Games

    @@ -89,14 +94,3 @@ function MainMenu({ ); } - -export default function MenuScreen() { - const profilePromise = commands.getProfile(); - const previousGamesPromise = commands.listGameHistories(); - - return ( - Loading profile

    }> - -
    - ); -} diff --git a/frontend/src/components/SetupScreen.tsx b/frontend/src/components/SetupScreen.tsx index f04a813..9bbba8e 100644 --- a/frontend/src/components/SetupScreen.tsx +++ b/frontend/src/components/SetupScreen.tsx @@ -1,13 +1,12 @@ import React from "react"; import { commands, PlayerProfile } from "@/bindings"; -import { unwrapResult } from "@/lib/result"; export default function SetupScreen() { const [displayName, setName] = React.useState("User"); const onSave = async () => { const profile = { display_name: displayName, pfp_base64: null } as PlayerProfile; - unwrapResult(await commands.completeSetup(profile)); + await commands.completeSetup(profile); }; return ( diff --git a/frontend/src/lib/hooks.ts b/frontend/src/lib/hooks.ts index 8bc6627..28b2038 100644 --- a/frontend/src/lib/hooks.ts +++ b/frontend/src/lib/hooks.ts @@ -1,17 +1,22 @@ import { useEffect } from "react"; import { events } from "@/bindings"; +import { SWRConfiguration } from "swr"; type ExtractCallback = ( payload: Parameters[0]>[0]["payload"] ) => void; +type SWRConfigTyp = { suspense: true } & SWRConfiguration; + +export const sharedSwrConfig: SWRConfigTyp = { suspense: true, dedupingInterval: 100 }; + /** * Convenience hook that does useEffect for a Tauri event and handles unsubscribing on unmount */ -export const useTauriEvent = ( +export function useTauriEvent( tauriEvent: E, cb: ExtractCallback -) => { +) { useEffect(() => { const unlisten = events[tauriEvent].listen((e) => { cb(e.payload); @@ -21,4 +26,4 @@ export const useTauriEvent = ( unlisten.then((f) => f()); }; }, [tauriEvent, cb]); -}; +} diff --git a/frontend/src/lib/result.ts b/frontend/src/lib/result.ts deleted file mode 100644 index e192cc7..0000000 --- a/frontend/src/lib/result.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Result } from "@/bindings.ts"; - -export const unwrapResult = (res: Result): T => { - switch (res.status) { - case "ok": - return res.data; - case "error": - throw res.error; - } -}; diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 913f447..cf00766 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -3,6 +3,25 @@ import ReactDOM from "react-dom/client"; const App = React.lazy(() => import("@/components/App")); +import { warn, debug, trace, info, error } from "@tauri-apps/plugin-log"; + +function forwardConsole( + fnName: "log" | "debug" | "info" | "warn" | "error", + logger: (message: string) => Promise +) { + const original = console[fnName]; + console[fnName] = (message) => { + original(message); + logger(message); + }; +} + +forwardConsole("log", trace); +forwardConsole("debug", debug); +forwardConsole("info", info); +forwardConsole("warn", warn); +forwardConsole("error", error); + ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 2fad73d..fb4ae69 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -4,7 +4,7 @@ import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; import path from "path"; -const host = process.env.TAURI_DEV_HOST; +const host = process.env.HOST_OVERRIDE || process.env.TAURI_DEV_HOST; export default defineConfig(async () => ({ plugins: [react()],