automatic rejoin on reload or server restart
This commit is contained in:
parent
feaf96c10e
commit
64a61ef2b3
|
@ -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;
|
||||
|
|
|
@ -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) =>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
|
3
tank-frontend/src/Guid.ts
Normal file
3
tank-frontend/src/Guid.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
export type Guid = `${string}-${string}-${string}-${string}-${string}`;
|
||||
|
||||
export const EmptyGuid: Guid = '00000000-0000-0000-0000-000000000000';
|
|
@ -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,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 <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'
|
||||
onChange={e => setName(e.target.value)}
|
||||
type="text"
|
||||
value={name}
|
||||
placeholder="player name"
|
||||
onChange={e => setName(e.target.value)}
|
||||
/>
|
||||
<button className="JoinElems"
|
||||
onClick={() => setClicked(true)}
|
||||
disabled={disableButtons}
|
||||
onClick={() => setClicked(true)}
|
||||
disabled={disableButtons}
|
||||
>
|
||||
join
|
||||
</button>
|
||||
|
|
|
@ -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>;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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<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'});
|
||||
}
|
||||
|
|
35
tank-frontend/src/useStoredState.ts
Normal file
35
tank-frontend/src/useStoredState.ts
Normal 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];
|
||||
}
|
Loading…
Reference in a new issue