From 64a61ef2b3dbb549f7b5df8d521eb29c5a5effc3 Mon Sep 17 00:00:00 2001 From: Vinzenz Schroeter Date: Sat, 13 Apr 2024 19:50:37 +0200 Subject: [PATCH] automatic rejoin on reload or server restart --- .../Interactivity/AppSerializerContext.cs | 1 + TanksServer/Program.cs | 4 +- tank-frontend/.env | 12 +++--- tank-frontend/src/ClientScreen.tsx | 4 +- tank-frontend/src/Controls.tsx | 7 ++-- tank-frontend/src/Guid.ts | 3 ++ tank-frontend/src/JoinForm.tsx | 38 ++++++++++--------- tank-frontend/src/PlayerInfo.tsx | 33 +++++++++------- tank-frontend/src/index.tsx | 31 +++++++++++---- tank-frontend/src/serverCalls.tsx | 31 +++++++++++---- tank-frontend/src/useStoredState.ts | 35 +++++++++++++++++ 11 files changed, 141 insertions(+), 58 deletions(-) create mode 100644 tank-frontend/src/Guid.ts create mode 100644 tank-frontend/src/useStoredState.ts diff --git a/TanksServer/Interactivity/AppSerializerContext.cs b/TanksServer/Interactivity/AppSerializerContext.cs index 2299059..12c4f4d 100644 --- a/TanksServer/Interactivity/AppSerializerContext.cs +++ b/TanksServer/Interactivity/AppSerializerContext.cs @@ -5,4 +5,5 @@ namespace TanksServer.Interactivity; [JsonSerializable(typeof(Player))] [JsonSerializable(typeof(IEnumerable))] [JsonSerializable(typeof(Guid))] +[JsonSerializable(typeof(NameId))] internal sealed partial class AppSerializerContext : JsonSerializerContext; diff --git a/TanksServer/Program.cs b/TanksServer/Program.cs index ee0ebf6..c7f0971 100644 --- a/TanksServer/Program.cs +++ b/TanksServer/Program.cs @@ -11,6 +11,8 @@ using TanksServer.Interactivity; namespace TanksServer; +internal sealed record class NameId(string Name, Guid Id); + public static class Program { public static void Main(string[] args) @@ -29,7 +31,7 @@ public static class Program { var player = playerService.GetOrAdd(name, id); return player != null - ? Results.Ok(player.Id) + ? Results.Ok(new NameId(name, id)) : Results.Unauthorized(); }); app.MapGet("/player", ([FromQuery] Guid id) => diff --git a/tank-frontend/.env b/tank-frontend/.env index d835d34..dd5866f 100644 --- a/tank-frontend/.env +++ b/tank-frontend/.env @@ -1,7 +1,7 @@ -#VITE_TANK_SCREEN_URL=ws://172.23.43.79/screen -#VITE_TANK_CONTROLS_URL=ws://172.23.43.79/controls -#VITE_TANK_PLAYER_URL=http://172.23.43.79/player +TANK_DOMAIN=vinzenz-lpt2 +VITE_TANK_API=http://$TANK_DOMAIN +VITE_TANK_WS=ws://$TANK_DOMAIN -VITE_TANK_SCREEN_URL=ws://vinzenz-lpt2/screen -VITE_TANK_CONTROLS_URL=ws://vinzenz-lpt2/controls -VITE_TANK_PLAYER_URL=http://vinzenz-lpt2/player +VITE_TANK_SCREEN_URL=$VITE_TANK_WS/screen +VITE_TANK_CONTROLS_URL=$VITE_TANK_WS/controls +VITE_TANK_PLAYER_URL=$VITE_TANK_API/player diff --git a/tank-frontend/src/ClientScreen.tsx b/tank-frontend/src/ClientScreen.tsx index e5353a0..a7eb341 100644 --- a/tank-frontend/src/ClientScreen.tsx +++ b/tank-frontend/src/ClientScreen.tsx @@ -41,7 +41,7 @@ function drawPixelsToCanvas(pixels: Uint8Array, canvas: HTMLCanvasElement) { drawContext.putImageData(imageData, 0, 0); } -export default function ClientScreen({}: {}) { +export default function ClientScreen({logout}: {logout: () => void}) { const canvasRef = useRef(null); const { @@ -50,7 +50,7 @@ export default function ClientScreen({}: {}) { sendMessage, getWebSocket } = useWebSocket(import.meta.env.VITE_TANK_SCREEN_URL, { - shouldReconnect: () => true, + onError: logout }); const socket = getWebSocket(); diff --git a/tank-frontend/src/Controls.tsx b/tank-frontend/src/Controls.tsx index 2e30aab..029dbc6 100644 --- a/tank-frontend/src/Controls.tsx +++ b/tank-frontend/src/Controls.tsx @@ -3,8 +3,9 @@ import useWebSocket from 'react-use-websocket'; import {useEffect} from 'react'; import {statusTextForReadyState} from './statusTextForReadyState.tsx'; -export default function Controls({playerId}: { - playerId: string +export default function Controls({playerId, logout}: { + playerId: string, + logout: () => void }) { const url = new URL(import.meta.env.VITE_TANK_CONTROLS_URL); url.searchParams.set('playerId', playerId); @@ -13,7 +14,7 @@ export default function Controls({playerId}: { getWebSocket, readyState } = useWebSocket(url.toString(), { - shouldReconnect: () => true, + onError: logout }); const socket = getWebSocket(); diff --git a/tank-frontend/src/Guid.ts b/tank-frontend/src/Guid.ts new file mode 100644 index 0000000..ce86dd0 --- /dev/null +++ b/tank-frontend/src/Guid.ts @@ -0,0 +1,3 @@ +export type Guid = `${string}-${string}-${string}-${string}-${string}`; + +export const EmptyGuid: Guid = '00000000-0000-0000-0000-000000000000'; diff --git a/tank-frontend/src/JoinForm.tsx b/tank-frontend/src/JoinForm.tsx index a62533c..fecca9d 100644 --- a/tank-frontend/src/JoinForm.tsx +++ b/tank-frontend/src/JoinForm.tsx @@ -1,8 +1,12 @@ -import { useEffect, useState } from 'react'; +import {useEffect, useState} from 'react'; import './JoinForm.css'; -import { PlayerResponse, postPlayer } from './serverCalls'; +import {NameId, PlayerResponse, postPlayer} from './serverCalls'; +import {Guid} from './Guid.ts'; -export default function JoinForm({ onDone }: { onDone: (id: string) => void }) { +export default function JoinForm({setNameId, clientId}: { + setNameId: (mutator: (oldState: NameId) => NameId) => void, + clientId: Guid +}) { const [name, setName] = useState(''); const [clicked, setClicked] = useState(false); const [data, setData] = useState(null); @@ -12,10 +16,10 @@ export default function JoinForm({ onDone }: { onDone: (id: string) => void }) { return; try { - postPlayer(name) - .then((value: PlayerResponse | null) => { + postPlayer({name, id: clientId}) + .then(value => { if (value) - onDone(value.id); + setNameId(prev => ({...prev, ...value})); else setClicked(false); }); @@ -23,27 +27,27 @@ export default function JoinForm({ onDone }: { onDone: (id: string) => void }) { console.log(e); alert(e); } - }, [clicked, setData, data]); + }, [clicked, setData, data, clientId, setClicked, setNameId]); const disableButtons = clicked || name.trim() === ''; - return
-

+ return
+

Tanks

-

Welcome and have fun!

+

Welcome and have fun!

-

+

Enter your name to join the game!

setName(e.target.value)} + type="text" + value={name} + placeholder="player name" + onChange={e => setName(e.target.value)} /> diff --git a/tank-frontend/src/PlayerInfo.tsx b/tank-frontend/src/PlayerInfo.tsx index d0670ca..2cbc6b6 100644 --- a/tank-frontend/src/PlayerInfo.tsx +++ b/tank-frontend/src/PlayerInfo.tsx @@ -1,12 +1,17 @@ -import { useEffect, useState } from 'react'; +import {useEffect, useState} from 'react'; import './PlayerInfo.css' -import { PlayerResponse, getPlayer } from './serverCalls'; +import {PlayerResponse, getPlayer} from './serverCalls'; -export default function PlayerInfo({ playerId }: { playerId: string }) { +export default function PlayerInfo({playerId, logout}: { playerId: string, logout: () => void }) { const [player, setPlayer] = useState(); const refresh = () => { - getPlayer(playerId).then(setPlayer); + getPlayer(playerId).then(value => { + if (value) + setPlayer(value); + else + logout(); + }); }; useEffect(() => { @@ -15,34 +20,34 @@ export default function PlayerInfo({ playerId }: { playerId: string }) { }); return
-

+

Tanks

-

+

name:

-

+

{player?.name}

-

+

kills:

-

- {player?.kills} +

+ {player?.scores.kills}

-

+

deaths:

-

- {player?.deaths} +

+ {player?.scores.deaths}

-
; +
; } diff --git a/tank-frontend/src/index.tsx b/tank-frontend/src/index.tsx index d6b278e..5f80438 100644 --- a/tank-frontend/src/index.tsx +++ b/tank-frontend/src/index.tsx @@ -1,24 +1,39 @@ -import React, { useState } from 'react'; +import React, {useCallback, useState} from 'react'; import './index.css'; import ClientScreen from './ClientScreen'; import Controls from './Controls.tsx'; import JoinForm from './JoinForm.tsx'; -import { createRoot } from 'react-dom/client'; +import {createRoot} from 'react-dom/client'; import PlayerInfo from './PlayerInfo.tsx'; +import {useStoredObjectState} from './useStoredState.ts'; +import {NameId, postPlayer} from './serverCalls.tsx'; function App() { - const [id, setId] = useState(null); + const [nameId, setNameId] = useStoredObjectState('access', () => ({ + id: crypto.randomUUID(), + name: '' + })); + + const [isLoggedIn, setLoggedIn] = useState(false); + const logout = () => setLoggedIn(false); + + useCallback(async () => { + if (isLoggedIn) + return; + const result = await postPlayer(nameId); + setLoggedIn(result !== null); + }, [nameId, isLoggedIn])(); return <> - {id === null && setId(name)} />} - {id == null || } - - {id == null || } + {nameId.name === '' && } + {isLoggedIn && } + + {isLoggedIn && } ; } createRoot(document.getElementById('root')!).render( - + ); diff --git a/tank-frontend/src/serverCalls.tsx b/tank-frontend/src/serverCalls.tsx index b6984d3..1905910 100644 --- a/tank-frontend/src/serverCalls.tsx +++ b/tank-frontend/src/serverCalls.tsx @@ -1,25 +1,42 @@ +import {Guid} from './Guid.ts'; + export type PlayerResponse = { readonly name: string; readonly id: string; - readonly kills: number; - readonly deaths: number; + readonly scores: { + readonly kills: number; + readonly deaths: number; + }; }; -export async function fetchTyped({ url, method }: { url: URL; method: string; }) { - const response = await fetch(url, { method }); +export type NameId = { + name: string, + id: Guid +}; + +export async function fetchTyped({url, method}: { url: URL; method: string; }) { + const response = await fetch(url, {method}); if (!response.ok) return null; return await response.json() as T; } -export function postPlayer(name: string) { +export function postPlayer({name, id}: NameId) { const url = new URL(import.meta.env.VITE_TANK_PLAYER_URL); url.searchParams.set('name', name); - return fetchTyped({ url, method: 'POST' }); + url.searchParams.set('id', id); + + return fetchTyped({url, method: 'POST'}); } export function getPlayer(id: string) { const url = new URL(import.meta.env.VITE_TANK_PLAYER_URL); url.searchParams.set('id', id); - return fetchTyped({ url, method: 'GET' }); + + return fetchTyped({url, method: 'GET'}); +} + +export function getScores() { + const url = new URL('/scores', import.meta.env.VITE_TANK_API); + return fetchTyped({url, method: 'GET'}); } diff --git a/tank-frontend/src/useStoredState.ts b/tank-frontend/src/useStoredState.ts new file mode 100644 index 0000000..212da05 --- /dev/null +++ b/tank-frontend/src/useStoredState.ts @@ -0,0 +1,35 @@ +import {useState} from 'react'; + +export function useStoredState(storageKey: string, initialState: () => string): [string, ((newState: string) => void)] { + const [state, setState] = useState(() => localStorage.getItem(storageKey) || initialState()); + + const setSavedState = (newState: string) => { + localStorage.setItem(storageKey, newState); + setState(newState); + }; + + return [state, setSavedState]; +} + +export function useStoredObjectState( + storageKey: string, + initialState: () => T +): [T, (mutator: (oldState: T) => T) => void] { + const getInitialState = () => { + const localStorageJson = localStorage.getItem(storageKey); + if (localStorageJson !== null && localStorageJson !== '') { + return JSON.parse(localStorageJson); + } + + return initialState(); + }; + + const [state, setState] = useState(getInitialState); + + const setSavedState = (mut: (oldState: T) => T) => { + localStorage.setItem(storageKey, JSON.stringify(mut(state))); + setState(mut); + }; + + return [state, setSavedState]; +}