automatic rejoin on reload or server restart

This commit is contained in:
Vinzenz Schroeter 2024-04-13 19:50:37 +02:00
parent feaf96c10e
commit 64a61ef2b3
11 changed files with 141 additions and 58 deletions

View file

@ -5,4 +5,5 @@ namespace TanksServer.Interactivity;
[JsonSerializable(typeof(Player))]
[JsonSerializable(typeof(IEnumerable<Player>))]
[JsonSerializable(typeof(Guid))]
[JsonSerializable(typeof(NameId))]
internal sealed partial class AppSerializerContext : JsonSerializerContext;

View file

@ -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) =>

View file

@ -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

View file

@ -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<HTMLCanvasElement>(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();

View file

@ -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();

View file

@ -0,0 +1,3 @@
export type Guid = `${string}-${string}-${string}-${string}-${string}`;
export const EmptyGuid: Guid = '00000000-0000-0000-0000-000000000000';

View file

@ -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<PlayerResponse | null>(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,22 +27,22 @@ 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 <div className='TankWelcome'>
<h1 className='JoinElems' style={{ "color": "white" }}>
return <div className="TankWelcome">
<h1 className="JoinElems" style={{'color': 'white'}}>
Tanks
</h1>
<p className='JoinElems' style={{ "color": "white" }}> Welcome and have fun!</p>
<p className="JoinElems" style={{'color': 'white'}}> Welcome and have fun!</p>
<div className="JoinForm">
<p className='JoinElems' style={{ "color": "white" }}>
<p className="JoinElems" style={{'color': 'white'}}>
Enter your name to join the game!
</p>
<input className="JoinElems"
type="text"
value={name}
placeholder='player name'
placeholder="player name"
onChange={e => setName(e.target.value)}
/>
<button className="JoinElems"

View file

@ -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<PlayerResponse | null>();
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 <div className='TankWelcome'>
<h1 className='Elems' style={{ "color": "white" }}>
<h1 className='Elems' style={{"color": "white"}}>
Tanks
</h1>
<div className="ScoreForm">
<div className='ElemGroup'>
<p className='Elems' style={{ "color": "white" }}>
<p className='Elems' style={{"color": "white"}}>
name:
</p>
<p className='Elems' style={{ "color": "white" }}>
<p className='Elems' style={{"color": "white"}}>
{player?.name}
</p>
</div>
<div className='ElemGroup'>
<p className='Elems' style={{ "color": "white" }}>
<p className='Elems' style={{"color": "white"}}>
kills:
</p>
<p className='Elems' style={{ "color": "white" }}>
{player?.kills}
<p className='Elems' style={{"color": "white"}}>
{player?.scores.kills}
</p>
</div>
<div className='ElemGroup'>
<p className='Elems' style={{ "color": "white" }}>
<p className='Elems' style={{"color": "white"}}>
deaths:
</p>
<p className='Elems' style={{ "color": "white" }}>
{player?.deaths}
<p className='Elems' style={{"color": "white"}}>
{player?.scores.deaths}
</p>
</div>
</div>
</div >;
</div>;
}

View file

@ -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<string | null>(null);
const [nameId, setNameId] = useStoredObjectState<NameId>('access', () => ({
id: crypto.randomUUID(),
name: ''
}));
const [isLoggedIn, setLoggedIn] = useState<boolean>(false);
const logout = () => setLoggedIn(false);
useCallback(async () => {
if (isLoggedIn)
return;
const result = await postPlayer(nameId);
setLoggedIn(result !== null);
}, [nameId, isLoggedIn])();
return <>
{id === null && <JoinForm onDone={name => setId(name)} />}
{id == null || <PlayerInfo playerId={id} />}
<ClientScreen />
{id == null || <Controls playerId={id} />}
{nameId.name === '' && <JoinForm setNameId={setNameId} clientId={nameId.id}/>}
{isLoggedIn && <PlayerInfo playerId={nameId.id} logout={logout}/>}
<ClientScreen logout={logout}/>
{isLoggedIn && <Controls playerId={nameId.id} logout={logout}/>}
</>;
}
createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
<App/>
</React.StrictMode>
);

View file

@ -1,25 +1,42 @@
import {Guid} from './Guid.ts';
export type PlayerResponse = {
readonly name: string;
readonly id: string;
readonly scores: {
readonly kills: number;
readonly deaths: number;
};
};
export async function fetchTyped<T>({ url, method }: { url: URL; method: string; }) {
const response = await fetch(url, { method });
export type NameId = {
name: string,
id: Guid
};
export async function fetchTyped<T>({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<PlayerResponse>({ url, method: 'POST' });
url.searchParams.set('id', id);
return fetchTyped<NameId>({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<PlayerResponse>({ url, method: 'GET' });
return fetchTyped<PlayerResponse>({url, method: 'GET'});
}
export function getScores() {
const url = new URL('/scores', import.meta.env.VITE_TANK_API);
return fetchTyped<PlayerResponse[]>({url, method: 'GET'});
}

View file

@ -0,0 +1,35 @@
import {useState} from 'react';
export function useStoredState(storageKey: string, initialState: () => string): [string, ((newState: string) => void)] {
const [state, setState] = useState<string>(() => localStorage.getItem(storageKey) || initialState());
const setSavedState = (newState: string) => {
localStorage.setItem(storageKey, newState);
setState(newState);
};
return [state, setSavedState];
}
export function useStoredObjectState<T>(
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<T>(getInitialState);
const setSavedState = (mut: (oldState: T) => T) => {
localStorage.setItem(storageKey, JSON.stringify(mut(state)));
setState(mut);
};
return [state, setSavedState];
}