feat(pong): reworked pong to use sane system as ttt
This commit is contained in:
parent
afd79e334c
commit
0b15fd897b
20 changed files with 637 additions and 707 deletions
|
|
@ -1,19 +0,0 @@
|
||||||
import { color } from './pong';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* function adds a message to the frontend pongMessage
|
|
||||||
* ATTENTION send inner HTML ******
|
|
||||||
* @param text
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
|
|
||||||
export function addPongMessage(text: string) {
|
|
||||||
const pongMessage = document.getElementById("system-box") as HTMLDivElement;
|
|
||||||
if (!pongMessage) return;
|
|
||||||
const messageElement = document.createElement("div-test");
|
|
||||||
messageElement.innerHTML = text;
|
|
||||||
pongMessage.appendChild(messageElement);
|
|
||||||
pongMessage.scrollTop = pongMessage.scrollHeight;
|
|
||||||
console.log(`%c DEBUG LOG: Added PONG new message:%c ${text}`, color.red, color.reset);
|
|
||||||
return ;
|
|
||||||
};
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
import { addPongMessage } from "./addPongMessage";
|
|
||||||
import { Socket } from 'socket.io-client';
|
|
||||||
import { getUser } from "@app/auth";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* function sends socket.emit to the backend to active and a broadcast message to all sockets
|
|
||||||
* echos addPongMessage() the message with addMessage to the sender
|
|
||||||
* @param socket
|
|
||||||
* @param msgCommand
|
|
||||||
*/
|
|
||||||
export function broadcastMsg (socket: Socket, msgCommand: string[]): void {
|
|
||||||
let msgText = msgCommand[1] ?? "";
|
|
||||||
addPongMessage(msgText);
|
|
||||||
const user = getUser();
|
|
||||||
if (user && socket?.connected) {
|
|
||||||
const message = {
|
|
||||||
command: msgCommand,
|
|
||||||
destination: '',
|
|
||||||
type: "chat",
|
|
||||||
user: user.name,
|
|
||||||
token: document.cookie,
|
|
||||||
text: msgText,
|
|
||||||
timestamp: Date.now(),
|
|
||||||
SenderWindowID: socket.id,
|
|
||||||
};
|
|
||||||
socket.emit('message', JSON.stringify(message));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
import { getUser } from "@app/auth";
|
|
||||||
import type { User } from '@app/auth'
|
|
||||||
/**
|
|
||||||
* function checks if logged in
|
|
||||||
* @returns either user | null
|
|
||||||
*/
|
|
||||||
export function isLoggedIn(): User | null {
|
|
||||||
return getUser() || null;
|
|
||||||
};
|
|
||||||
|
|
@ -30,7 +30,7 @@
|
||||||
<div id="batleft" class="pong-batleft bg-amber-400 top-0"></div>
|
<div id="batleft" class="pong-batleft bg-amber-400 top-0"></div>
|
||||||
<div class="pong-center-line"></div>
|
<div class="pong-center-line"></div>
|
||||||
<div id="batright" class="pong-batright bg-amber-400 top-0"></div>
|
<div id="batright" class="pong-batright bg-amber-400 top-0"></div>
|
||||||
<div id="ball" class="w-8 h-8 rounded-full border-4 bg-white border-gray-400"></div>
|
<div id="ball" class="rounded-full border-4 bg-white border-gray-400"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,276 +1,36 @@
|
||||||
import { addRoute, setTitle, type RouteHandlerParams, type RouteHandlerReturn } from "@app/routing";
|
import { addRoute, setTitle, type RouteHandlerParams, type RouteHandlerReturn } from "@app/routing";
|
||||||
import { showError } from "@app/toast";
|
|
||||||
import authHtml from './pong.html?raw';
|
import authHtml from './pong.html?raw';
|
||||||
import client from '@app/api'
|
import io from 'socket.io-client';
|
||||||
import { getUser, updateUser } from "@app/auth";
|
import type { CSocket, GameMove, GameUpdate } from "./socket";
|
||||||
import io, { Socket } from 'socket.io-client';
|
import { showError, showInfo } from "@app/toast";
|
||||||
import { addPongMessage } from './addPongMessage';
|
|
||||||
import { isLoggedIn } from './isLoggedIn';
|
|
||||||
import type { ClientMessage, ClientProfil } from './types_front';
|
|
||||||
import { isNullish } from "@app/utils";
|
|
||||||
|
|
||||||
export const color = {
|
|
||||||
red: 'color: red;',
|
|
||||||
green: 'color: green;',
|
|
||||||
yellow: 'color: orange;',
|
|
||||||
blue: 'color: blue;',
|
|
||||||
reset: '',
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO: local game (2player -> server -> 2player : current setup)
|
// TODO: local game (2player -> server -> 2player : current setup)
|
||||||
// TODO: tournament via remote (dedicated queu? idk)
|
// TODO: tournament via remote (dedicated queu? idk)
|
||||||
//
|
//
|
||||||
|
|
||||||
// get the name of the machine used to connect
|
// get the name of the machine used to connect
|
||||||
const machineHostName = window.location.hostname;
|
declare module 'ft_state' {
|
||||||
console.log('connect to login at %chttps://' + machineHostName + ':8888/app/login',color.yellow);
|
interface State {
|
||||||
|
pongSock?: CSocket;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export let __socket: Socket | undefined = undefined;
|
document.addEventListener("ft:pageChange", () => {
|
||||||
|
if (window.__state.pongSock !== undefined) window.__state.pongSock.close();
|
||||||
document.addEventListener('ft:pageChange', () => { // dont regen socket on page change from forward/backward navigation arrows
|
window.__state.pongSock = undefined;
|
||||||
if (__socket !== undefined)
|
|
||||||
__socket.close();
|
|
||||||
__socket = undefined;
|
|
||||||
console.log("Page changed");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
export function getSocket(): CSocket {
|
||||||
* @returns the initialized socket
|
if (window.__state.pongSock === undefined)
|
||||||
*/
|
window.__state.pongSock = io(window.location.host, { path: "/api/pong/socket.io/" }) as any as CSocket;
|
||||||
export function getSocket(): Socket {
|
return window.__state.pongSock;
|
||||||
let addressHost = `wss://${machineHostName}:8888`;
|
|
||||||
|
|
||||||
if (__socket === undefined)
|
|
||||||
__socket = io(addressHost, {
|
|
||||||
path: "/api/pong/socket.io/",
|
|
||||||
secure: false,
|
|
||||||
transports: ["websocket"],
|
|
||||||
});
|
|
||||||
return __socket;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param socket The socket to wait for
|
|
||||||
* @returns voir or a promise<void>
|
|
||||||
*/
|
|
||||||
function waitSocketConnected(socket: Socket): Promise<void> {
|
|
||||||
return new Promise(resolve => {
|
|
||||||
if (socket.connected) return resolve();
|
|
||||||
socket.on("connect", () => resolve());
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param socket The socket to communicat
|
|
||||||
* @returns nothing
|
|
||||||
*/
|
|
||||||
async function whoami(socket: Socket) {
|
|
||||||
try {
|
|
||||||
const chatWindow = document.getElementById("t-chatbox") as HTMLDivElement;
|
|
||||||
|
|
||||||
const res = await client.guestLogin();
|
|
||||||
switch (res.kind) {
|
|
||||||
case 'success': {
|
|
||||||
let user = await updateUser();
|
|
||||||
if (chatWindow) {
|
|
||||||
socket.emit('updateClientName', {
|
|
||||||
oldUser: '',
|
|
||||||
user: user?.name
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
if (user === null)
|
|
||||||
return showError('Failed to get user: no user ?');
|
|
||||||
setTitle(`Welcome ${user.guest ? '[GUEST] ' : ''}${user.name}`);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'failed': {
|
|
||||||
showError(`Failed to login: ${res.msg}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Login error:", e);
|
|
||||||
showError('Failed to login: Unknown error');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
function pongClient(_url: string, _args: RouteHandlerParams): RouteHandlerReturn {
|
function pongClient(_url: string, _args: RouteHandlerParams): RouteHandlerReturn {
|
||||||
let socket = getSocket();
|
|
||||||
|
|
||||||
socket.on("connect", async () => {
|
|
||||||
const systemWindow = document.getElementById('system-box') as HTMLDivElement;
|
|
||||||
await waitSocketConnected(socket);
|
|
||||||
console.log("I AM Connected to the server:", socket.id);
|
|
||||||
// const message = {
|
|
||||||
// command: "",
|
|
||||||
// destination: 'system-info',
|
|
||||||
// type: "chat",
|
|
||||||
// user: getUser()?.name,
|
|
||||||
// token: document.cookie ?? "",
|
|
||||||
// text: " has Just ARRIVED in the chat",
|
|
||||||
// timestamp: Date.now(),
|
|
||||||
// SenderWindowID: socket.id,
|
|
||||||
// };
|
|
||||||
// socket.emit('message', JSON.stringify(message));
|
|
||||||
const messageElement = document.createElement("div");
|
|
||||||
// messageElement.textContent = `${message.user}: is connected au server`;
|
|
||||||
messageElement.textContent = `${getUser()?.name ?? "unkown user"}: is connected au server`;
|
|
||||||
systemWindow.appendChild(messageElement);
|
|
||||||
systemWindow.scrollTop = systemWindow.scrollHeight;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Queu handler
|
|
||||||
async function joinQueu(socket : Socket) {
|
|
||||||
try {
|
|
||||||
const res = await client.guestLogin();
|
|
||||||
|
|
||||||
switch (res.kind) {
|
|
||||||
case 'success': {
|
|
||||||
let user = await updateUser();
|
|
||||||
|
|
||||||
if (user === null)
|
|
||||||
return showError('Failed to get user: no user ?');
|
|
||||||
socket.emit('queuJoin', user.id);
|
|
||||||
console.log('queu join sent for : ', user.id);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'failed': {
|
|
||||||
showError(`Failed to Join Game Queu: ${res.msg}`);
|
|
||||||
console.log('Failed to Join Game Queu');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (err ) {
|
|
||||||
showError(`Failed to Join Game Queu`);
|
|
||||||
console.log('Failed to Join Game Queu');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// keys handler
|
|
||||||
const keys: Record<string, boolean> = {};
|
|
||||||
|
|
||||||
document.addEventListener("keydown", (e) => {
|
|
||||||
keys[e.key.toLowerCase()] = true;
|
|
||||||
});
|
|
||||||
document.addEventListener("keyup", (e) => {
|
|
||||||
keys[e.key.toLowerCase()] = false;
|
|
||||||
});
|
|
||||||
setInterval(() => { // key sender
|
|
||||||
if ((keys['w'] || keys['s']) && !(keys['w'] && keys['s'])) { // exclusive or to filter requests
|
|
||||||
if (keys['w']) {
|
|
||||||
socket.emit("batmove_Left", "up");
|
|
||||||
console.log('north key pressed - emit batmove_Left up');
|
|
||||||
}
|
|
||||||
if (keys['s']) {
|
|
||||||
socket.emit("batmove_Left", "down");
|
|
||||||
console.log('south key pressed - emit batmove_Left down');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ((keys['p'] || keys['l']) && !(keys['p'] && keys['l'])) { // exclusive or to filter requests
|
|
||||||
if (keys['p']) {
|
|
||||||
socket.emit("batmove_Right", "up");
|
|
||||||
console.log('north key pressed - emit batmove_Right up');
|
|
||||||
}
|
|
||||||
if (keys['l']) {
|
|
||||||
socket.emit("batmove_Right", "down");
|
|
||||||
console.log('south key pressed - emit batmove_Right down');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, 16);
|
|
||||||
|
|
||||||
// Pong Objects updators
|
|
||||||
socket.on("batLeft_update", (y: number) => {
|
|
||||||
console.log('batLeft_update received y: ', y);
|
|
||||||
const bat = document.getElementById("batleft") as HTMLDivElement | null;
|
|
||||||
if (!bat) {
|
|
||||||
console.error("FATAL ERROR: Bat element with ID 'bat-left' not found. Check HTML.");
|
|
||||||
return ;
|
|
||||||
}
|
|
||||||
if (typeof y === 'number' && !isNaN(y)) {
|
|
||||||
bat.style.transform = `translateY(${y}px)`;
|
|
||||||
} else {
|
|
||||||
console.warn(`Received invalid Y value: ${y}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
socket.on("batRight_update", (y: number) => {
|
|
||||||
console.log('batRight_update received y: ', y);
|
|
||||||
const bat = document.getElementById("batright") as HTMLDivElement | null;
|
|
||||||
if (!bat) {
|
|
||||||
console.error("FATAL ERROR: Bat element with ID 'bat-Right' not found. Check HTML.");
|
|
||||||
return ;
|
|
||||||
}
|
|
||||||
if (typeof y === 'number' && !isNaN(y)) {
|
|
||||||
bat.style.transform = `translateY(${y}px)`;
|
|
||||||
} else {
|
|
||||||
console.warn(`Received invalid Y value: ${y}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
socket.on("ballPos_update", (x:number, y : number) => {
|
|
||||||
console.log('ballPos_update recieved');
|
|
||||||
const ball = document.getElementById("ball") as HTMLDivElement | null;
|
|
||||||
|
|
||||||
if (!ball) {
|
|
||||||
console.error("FATAL ERROR: Bat element with ID 'bat-Right' not found. Check HTML.");
|
|
||||||
return ;
|
|
||||||
}
|
|
||||||
if (typeof y !== 'number' || isNaN(y) || typeof x !== 'number' || isNaN(x)) {
|
|
||||||
console.warn(`Received invalid X/Y value: ${x} / ${y}`);
|
|
||||||
return ;
|
|
||||||
}
|
|
||||||
ball.style.transform = `translateY(${y}px)`;
|
|
||||||
ball.style.transform += `translateX(${x}px)`;
|
|
||||||
});
|
|
||||||
|
|
||||||
// socket.once('welcome', (data) => {
|
|
||||||
// console.log('%cWelcome PONG PAGE', color.yellow );
|
|
||||||
// addPongMessage('socket.once \'Welcome\' called')
|
|
||||||
// });
|
|
||||||
|
|
||||||
// Listen for messages from the server "MsgObjectServer"
|
|
||||||
socket.on("MsgObjectServer", (data: { message: ClientMessage}) => {
|
|
||||||
// Display the message in the chat window
|
|
||||||
console.log("message recieved : ", data.message.text);
|
|
||||||
const systemWindow = document.getElementById('system-box') as HTMLDivElement;
|
|
||||||
const MAX_SYSTEM_MESSAGES = 10;
|
|
||||||
|
|
||||||
if (systemWindow && data.message.destination === "system-info") {
|
|
||||||
const messageElement = document.createElement("div");
|
|
||||||
messageElement.textContent = `${data.message.user}: ${data.message.text}`;
|
|
||||||
systemWindow.appendChild(messageElement);
|
|
||||||
|
|
||||||
// keep only last 10
|
|
||||||
while (systemWindow.children.length > MAX_SYSTEM_MESSAGES) {
|
|
||||||
systemWindow.removeChild(systemWindow.firstChild!);
|
|
||||||
}
|
|
||||||
systemWindow.scrollTop = systemWindow.scrollHeight;
|
|
||||||
}
|
|
||||||
if (systemWindow && data.message.destination === "score-info") {
|
|
||||||
console.log("score update:", data.message.text);
|
|
||||||
const scoreboard = document.getElementById('score-board') as HTMLHeadingElement ;
|
|
||||||
|
|
||||||
if (!scoreboard) {
|
|
||||||
console.log("update score failed :(");
|
|
||||||
return ;
|
|
||||||
}
|
|
||||||
scoreboard.textContent = `${data.message.text}`;
|
|
||||||
}
|
|
||||||
// console.log("Getuser():", getUser());
|
|
||||||
});
|
|
||||||
|
|
||||||
setTitle('Pong Game Page');
|
setTitle('Pong Game Page');
|
||||||
return {
|
return {
|
||||||
|
|
||||||
html: authHtml, postInsert: async (app) => {
|
html: authHtml, postInsert: async (app) => {
|
||||||
const bwhoami = document.getElementById('b-whoami') as HTMLButtonElement;
|
|
||||||
const bqueu = document.getElementById('b-joinQueu') as HTMLButtonElement;
|
|
||||||
|
|
||||||
bwhoami?.addEventListener('click', async () => {
|
|
||||||
whoami(socket);
|
|
||||||
});
|
|
||||||
bqueu?.addEventListener('click', async () => {
|
|
||||||
joinQueu(socket);
|
|
||||||
});
|
|
||||||
|
|
||||||
const checkbox = document.getElementById("modeToggle") as HTMLInputElement;
|
const checkbox = document.getElementById("modeToggle") as HTMLInputElement;
|
||||||
const label = document.getElementById("toggleLabel") as HTMLSpanElement;
|
const label = document.getElementById("toggleLabel") as HTMLSpanElement;
|
||||||
const track = document.getElementById("toggleTrack") as HTMLDivElement;
|
const track = document.getElementById("toggleTrack") as HTMLDivElement;
|
||||||
|
|
@ -287,7 +47,67 @@ function pongClient(_url: string, _args: RouteHandlerParams): RouteHandlerReturn
|
||||||
knob.classList.remove("translate-x-7");
|
knob.classList.remove("translate-x-7");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const batLeft = document.querySelector<HTMLDivElement>("#batleft");
|
||||||
|
const batRight = document.querySelector<HTMLDivElement>("#batright");
|
||||||
|
const ball = document.querySelector<HTMLDivElement>("#ball");
|
||||||
|
const score = document.querySelector<HTMLDivElement>("#score-board");
|
||||||
|
if (!batLeft || !batRight || !ball || !score)
|
||||||
|
return showError('fatal error');
|
||||||
|
|
||||||
|
let socket = getSocket();
|
||||||
|
|
||||||
|
// keys handler
|
||||||
|
const keys: Record<string, boolean> = {};
|
||||||
|
|
||||||
|
document.addEventListener("keydown", (e) => {
|
||||||
|
keys[e.key.toLowerCase()] = true;
|
||||||
|
});
|
||||||
|
document.addEventListener("keyup", (e) => {
|
||||||
|
keys[e.key.toLowerCase()] = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
setInterval(() => { // key sender
|
||||||
|
let packet: GameMove = {
|
||||||
|
move: null,
|
||||||
|
}
|
||||||
|
if ((keys['w'] !== keys['s'])) {
|
||||||
|
packet.move = keys['w'] ? 'up' : 'down';
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.emit('gameMove', packet);
|
||||||
|
}, 1000 / 60);
|
||||||
|
|
||||||
|
const render = (state: GameUpdate) => {
|
||||||
|
//batLeft.style.transform = `translateY(${state.left.paddle.y}px) translateX(${state.left.paddle.x}px)`;
|
||||||
|
batLeft.style.top = `${state.left.paddle.y}px`;
|
||||||
|
batLeft.style.left = `${state.left.paddle.x}px`;
|
||||||
|
batLeft.style.width = `${state.left.paddle.width}px`;
|
||||||
|
batLeft.style.height = `${state.left.paddle.height}px`;
|
||||||
|
|
||||||
|
//batRight.style.transform = `translateY(${state.right.paddle.y}px) translateX(-${state.left.paddle.x}px)`;
|
||||||
|
batRight.style.top = `${state.right.paddle.y}px`;
|
||||||
|
batRight.style.left = `${state.right.paddle.x}px`;
|
||||||
|
batRight.style.width = `${state.right.paddle.width}px`;
|
||||||
|
batRight.style.height = `${state.right.paddle.height}px`;
|
||||||
|
|
||||||
|
ball.style.transform = `translateX(${state.ball.x - state.ball.size}px) translateY(${state.ball.y - state.ball.size}px)`;
|
||||||
|
ball.style.height = `${state.ball.size * 2}px`;
|
||||||
|
ball.style.width = `${state.ball.size * 2}px`;
|
||||||
|
|
||||||
|
|
||||||
|
score.innerText = `${state.left.score} | ${state.right.score}`
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.on('gameUpdate', (state: GameUpdate) => render(state));
|
||||||
|
socket.on('newGame', (state) => render(state));
|
||||||
|
|
||||||
|
socket.on('updateInformation', (e) => showInfo(`UpdateInformation: t=${e.totalUser};q=${e.inQueue}`));
|
||||||
|
socket.on('queueEvent', (e) => showInfo(`QueueEvent: ${e}`));
|
||||||
|
socket.emit('enqueue');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
addRoute('/pong', pongClient, { bypass_auth: true });
|
addRoute('/pong', pongClient);
|
||||||
|
|
|
||||||
47
frontend/src/pages/pong/socket.ts
Normal file
47
frontend/src/pages/pong/socket.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
import { Socket } from 'socket.io-client';
|
||||||
|
|
||||||
|
export type UpdateInfo = {
|
||||||
|
inQueue: number,
|
||||||
|
totalUser: number,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PaddleData = {
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GameUpdate = {
|
||||||
|
gameId: string;
|
||||||
|
|
||||||
|
left: { id: string, paddle: PaddleData, score: number };
|
||||||
|
right: { id: string, paddle: PaddleData, score: number };
|
||||||
|
|
||||||
|
ball: { x: number, y: number, size: number };
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GameMove = {
|
||||||
|
move: 'up' | 'down' | null,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ClientToServer {
|
||||||
|
enqueue: () => void;
|
||||||
|
dequeue: () => void;
|
||||||
|
debugInfo: () => void;
|
||||||
|
gameMove: (up: GameMove) => void;
|
||||||
|
connectedToGame: (gameId: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface ServerToClient {
|
||||||
|
forceDisconnect: (reason: string) => void;
|
||||||
|
queueEvent: (msg: 'registered' | 'unregistered') => void;
|
||||||
|
updateInformation: (info: UpdateInfo) => void,
|
||||||
|
newGame: (initState: GameUpdate) => void,
|
||||||
|
gameUpdate: (state: GameUpdate) => void,
|
||||||
|
gameEnd: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SSocket = Socket<ClientToServer, ServerToClient>;
|
||||||
|
export type CSocket = Socket<ServerToClient, ClientToServer>;
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
export type ClientMessage = {
|
|
||||||
command: string
|
|
||||||
destination: string;
|
|
||||||
user: string;
|
|
||||||
text: string;
|
|
||||||
SenderWindowID: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ClientProfil = {
|
|
||||||
command: string,
|
|
||||||
destination: string,
|
|
||||||
type: string,
|
|
||||||
user: string,
|
|
||||||
loginName: string,
|
|
||||||
userID: string,
|
|
||||||
text: string,
|
|
||||||
timestamp: number,
|
|
||||||
SenderWindowID:string,
|
|
||||||
SenderName: string,
|
|
||||||
Sendertext: string,
|
|
||||||
innerHtml?: string,
|
|
||||||
};
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
|
|
||||||
set -e
|
|
||||||
set -x
|
|
||||||
# do anything here
|
|
||||||
|
|
||||||
# run the CMD [ ... ] from the dockerfile
|
|
||||||
exec "$@"
|
|
||||||
12
src/pong/src/@types/socket.io.d.ts
vendored
Normal file
12
src/pong/src/@types/socket.io.d.ts
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { type UserId } from '@shared/database/mixin/user';
|
||||||
|
|
||||||
|
declare module 'socket.io'
|
||||||
|
{
|
||||||
|
interface Socket {
|
||||||
|
authUser: {
|
||||||
|
id: UserId;
|
||||||
|
name: string;
|
||||||
|
guest: boolean;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -6,32 +6,11 @@ import * as auth from '@shared/auth';
|
||||||
import * as swagger from '@shared/swagger';
|
import * as swagger from '@shared/swagger';
|
||||||
import * as utils from '@shared/utils';
|
import * as utils from '@shared/utils';
|
||||||
import { Server, Socket } from 'socket.io';
|
import { Server, Socket } from 'socket.io';
|
||||||
import { broadcast } from './broadcast';
|
import { newState, State } from './state';
|
||||||
import type { ClientProfil, ClientMessage } from './chat_types';
|
import { ClientToServer, ServerToClient } from './socket';
|
||||||
import { sendInvite } from './sendInvite';
|
|
||||||
import { setGameLink } from './setGameLink';
|
|
||||||
import { UserId } from '@shared/database/mixin/user';
|
|
||||||
|
|
||||||
// colors for console.log
|
|
||||||
export const color = {
|
|
||||||
red: '\x1b[31m',
|
|
||||||
green: '\x1b[32m',
|
|
||||||
yellow: '\x1b[33m',
|
|
||||||
blue: '\x1b[34m',
|
|
||||||
reset: '\x1b[0m',
|
|
||||||
};
|
|
||||||
|
|
||||||
declare const __SERVICE_NAME: string;
|
declare const __SERVICE_NAME: string;
|
||||||
|
|
||||||
// Global map of clients
|
|
||||||
// key = socket, value = clientname
|
|
||||||
interface ClientInfo {
|
|
||||||
user: string;
|
|
||||||
lastSeen: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const clientChat = new Map<string, ClientInfo>();
|
|
||||||
|
|
||||||
// @ts-expect-error: import.meta.glob is a vite thing. Typescript doesn't know this...
|
// @ts-expect-error: import.meta.glob is a vite thing. Typescript doesn't know this...
|
||||||
const plugins = import.meta.glob('./plugins/**/*.ts', { eager: true });
|
const plugins = import.meta.glob('./plugins/**/*.ts', { eager: true });
|
||||||
// @ts-expect-error: import.meta.glob is a vite thing. Typescript doesn't know this...
|
// @ts-expect-error: import.meta.glob is a vite thing. Typescript doesn't know this...
|
||||||
|
|
@ -60,6 +39,7 @@ const app: FastifyPluginAsync = async (fastify, opts): Promise<void> => {
|
||||||
|
|
||||||
fastify.ready((err) => {
|
fastify.ready((err) => {
|
||||||
if (err) throw err;
|
if (err) throw err;
|
||||||
|
newState(fastify);
|
||||||
onReady(fastify);
|
onReady(fastify);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
@ -69,266 +49,13 @@ export { app };
|
||||||
// When using .decorate you have to specify added properties for Typescript
|
// When using .decorate you have to specify added properties for Typescript
|
||||||
declare module 'fastify' {
|
declare module 'fastify' {
|
||||||
interface FastifyInstance {
|
interface FastifyInstance {
|
||||||
io: Server<{
|
io: Server<ClientToServer, ServerToClient>;
|
||||||
inviteGame: (data: ClientProfil) => void;
|
|
||||||
message: (msg: string) => void;
|
|
||||||
batmove_Left: (direction: 'up' | 'down') => void;
|
|
||||||
batmove_Right: (direction: 'up' | 'down') => void;
|
|
||||||
batLeft_update: (y: number) => void;
|
|
||||||
batRight_update: (y: number) => void;
|
|
||||||
ballPos_update: (x: number, y: number) => void;
|
|
||||||
MsgObjectServer: (data: { message: ClientMessage }) => void;
|
|
||||||
queuJoin: (userID: UserId) => void;
|
|
||||||
}>;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function isInRange(x: number, low: number, high: number) {
|
|
||||||
if (x >= low && x <= high) return true;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function sendScore(
|
|
||||||
socket: Socket,
|
|
||||||
scoreLeft: number,
|
|
||||||
scoreRight: number,
|
|
||||||
) {
|
|
||||||
// idk why, sometimes... it fails?
|
|
||||||
const msg: ClientMessage = {
|
|
||||||
destination: 'score-info',
|
|
||||||
command: '',
|
|
||||||
user: '',
|
|
||||||
text: scoreLeft.toString() + ':' + scoreRight.toString(),
|
|
||||||
SenderWindowID: '',
|
|
||||||
};
|
|
||||||
|
|
||||||
socket.emit('MsgObjectServer', { message: msg });
|
|
||||||
}
|
|
||||||
|
|
||||||
async function onReady(fastify: FastifyInstance) {
|
async function onReady(fastify: FastifyInstance) {
|
||||||
// shows address for connection au server transcendance
|
|
||||||
const session = process.env.SESSION_MANAGER ?? '';
|
|
||||||
if (session) {
|
|
||||||
const part = session.split('/')[1];
|
|
||||||
const machineName = part.split('.')[0];
|
|
||||||
console.log(
|
|
||||||
color.yellow,
|
|
||||||
'Connect at : https://' + machineName + ':8888/app/login',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// DRAW AREA
|
|
||||||
// top edge of the field
|
|
||||||
const TOP_EDGE = 0;
|
|
||||||
// bottom edge of the field;
|
|
||||||
const BOTTOM_EDGE = 450;
|
|
||||||
const LEFT_EDGE = 0;
|
|
||||||
const RIGHT_EDGE = 800;
|
|
||||||
|
|
||||||
void LEFT_EDGE;
|
|
||||||
|
|
||||||
// PADDLEs
|
|
||||||
const PADDLE_HEIGHT = 80;
|
|
||||||
const PADDLE_WIDTH = 12;
|
|
||||||
|
|
||||||
const PADDLE_SPEED = 20;
|
|
||||||
const PADDLE_X_OFFSET = 4;
|
|
||||||
|
|
||||||
// 370
|
|
||||||
const MAX_PADDLE_Y = BOTTOM_EDGE - PADDLE_HEIGHT;
|
|
||||||
// 185
|
|
||||||
const PADDLE_START = BOTTOM_EDGE / 2 - PADDLE_HEIGHT / 2;
|
|
||||||
|
|
||||||
// BALL
|
|
||||||
// widht times 2 bc rounded on moth sides + 4 for border
|
|
||||||
const BALL_SIZE = 8 * 2 + 4;
|
|
||||||
const START_BALLX = RIGHT_EDGE / 2 - BALL_SIZE;
|
|
||||||
const START_BALLY = BOTTOM_EDGE / 2 - BALL_SIZE;
|
|
||||||
|
|
||||||
const ACCELERATION_FACTOR = 1.15;
|
|
||||||
const ABS_MAX_BALL_SPEED = 3;
|
|
||||||
|
|
||||||
// val inits
|
|
||||||
// shared start bat position
|
|
||||||
let paddleLeft = PADDLE_START;
|
|
||||||
// shared start bat position
|
|
||||||
let paddleRight = PADDLE_START;
|
|
||||||
|
|
||||||
let ballPosX = START_BALLX;
|
|
||||||
let ballPosY = START_BALLY;
|
|
||||||
let ballSpeedX = -1;
|
|
||||||
let ballSpeedY = -1;
|
|
||||||
let scoreL = 0;
|
|
||||||
let scoreR = 0;
|
|
||||||
|
|
||||||
// uuid, game uid - if not in game empty string
|
|
||||||
const games: Record<UserId, string> = {};
|
|
||||||
|
|
||||||
fastify.io.on('connection', (socket: Socket) => {
|
fastify.io.on('connection', (socket: Socket) => {
|
||||||
socket.emit('batLeft_update', paddleLeft);
|
fastify.log.info(`Client connected: ${socket.id}`);
|
||||||
socket.emit('batRight_update', paddleRight);
|
State.registerUser(socket);
|
||||||
socket.emit('ballPos_update', ballPosX, ballPosY);
|
|
||||||
sendScore(socket, scoreL, scoreR);
|
|
||||||
|
|
||||||
// GAME
|
|
||||||
// paddle handling
|
|
||||||
socket.on('batmove_Left', (direction: 'up' | 'down') => {
|
|
||||||
if (direction === 'up') {
|
|
||||||
paddleLeft -= PADDLE_SPEED;
|
|
||||||
}
|
|
||||||
if (direction === 'down') {
|
|
||||||
paddleLeft += PADDLE_SPEED;
|
|
||||||
}
|
|
||||||
// position of bat leftplokoplpl
|
|
||||||
paddleLeft = Math.max(TOP_EDGE, Math.min(MAX_PADDLE_Y, paddleLeft));
|
|
||||||
console.log('batLeft_update:', paddleLeft);
|
|
||||||
socket.emit('batLeft_update', paddleLeft);
|
|
||||||
});
|
|
||||||
socket.on('batmove_Right', (direction: 'up' | 'down') => {
|
|
||||||
if (direction === 'up') {
|
|
||||||
paddleRight -= PADDLE_SPEED;
|
|
||||||
}
|
|
||||||
if (direction === 'down') {
|
|
||||||
paddleRight += PADDLE_SPEED;
|
|
||||||
}
|
|
||||||
// position of bat left
|
|
||||||
paddleRight = Math.max(
|
|
||||||
TOP_EDGE,
|
|
||||||
Math.min(MAX_PADDLE_Y, paddleRight),
|
|
||||||
);
|
|
||||||
socket.emit('batRight_update', paddleRight);
|
|
||||||
});
|
|
||||||
// ball handling:
|
|
||||||
setInterval(async () => {
|
|
||||||
const new_ballPosX = ballPosX + ballSpeedX;
|
|
||||||
const new_ballPosY = ballPosY + ballSpeedY;
|
|
||||||
|
|
||||||
if (
|
|
||||||
((isInRange(
|
|
||||||
new_ballPosY,
|
|
||||||
paddleLeft,
|
|
||||||
paddleLeft + PADDLE_HEIGHT,
|
|
||||||
) ||
|
|
||||||
isInRange(
|
|
||||||
new_ballPosY + BALL_SIZE * 2,
|
|
||||||
paddleLeft,
|
|
||||||
paddleLeft + PADDLE_HEIGHT,
|
|
||||||
)) &&
|
|
||||||
// y ok ?
|
|
||||||
isInRange(
|
|
||||||
new_ballPosX,
|
|
||||||
PADDLE_X_OFFSET,
|
|
||||||
PADDLE_X_OFFSET + PADDLE_WIDTH,
|
|
||||||
) &&
|
|
||||||
ballSpeedX < 0) ||
|
|
||||||
// x ok? && ball going toward paddle?
|
|
||||||
((isInRange(
|
|
||||||
new_ballPosY,
|
|
||||||
paddleRight,
|
|
||||||
paddleRight + PADDLE_HEIGHT,
|
|
||||||
) ||
|
|
||||||
isInRange(
|
|
||||||
new_ballPosY + BALL_SIZE * 2,
|
|
||||||
paddleRight,
|
|
||||||
paddleRight + PADDLE_HEIGHT,
|
|
||||||
)) &&
|
|
||||||
// right side equations
|
|
||||||
isInRange(
|
|
||||||
new_ballPosX + BALL_SIZE * 2,
|
|
||||||
RIGHT_EDGE - PADDLE_X_OFFSET - PADDLE_WIDTH,
|
|
||||||
RIGHT_EDGE - PADDLE_X_OFFSET,
|
|
||||||
) &&
|
|
||||||
ballSpeedX > 0)
|
|
||||||
) {
|
|
||||||
ballSpeedX *= -1;
|
|
||||||
ballSpeedX *= ACCELERATION_FACTOR;
|
|
||||||
ballSpeedY *= ACCELERATION_FACTOR;
|
|
||||||
console.log('bat colision');
|
|
||||||
}
|
|
||||||
else if (
|
|
||||||
new_ballPosX < 0 ||
|
|
||||||
new_ballPosX + BALL_SIZE * 2 > RIGHT_EDGE
|
|
||||||
) {
|
|
||||||
ballPosX = START_BALLX;
|
|
||||||
ballPosY = START_BALLY;
|
|
||||||
ballSpeedX = Math.random() - 0.5 < 0 ? -1 : 1;
|
|
||||||
|
|
||||||
if (new_ballPosX < 0) {
|
|
||||||
scoreR += 1;
|
|
||||||
ballSpeedY = -1;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
scoreL += 1;
|
|
||||||
ballSpeedY = 1;
|
|
||||||
}
|
|
||||||
if (scoreL >= 5 || scoreR >= 5) {
|
|
||||||
console.log('game should stop + board reset');
|
|
||||||
// temp solution
|
|
||||||
ballSpeedX = 0;
|
|
||||||
ballSpeedY = 0;
|
|
||||||
// reset board :D
|
|
||||||
}
|
|
||||||
console.log('point scored');
|
|
||||||
sendScore(socket, scoreL, scoreR);
|
|
||||||
// TODO: score point + ball reset + spd reset
|
|
||||||
}
|
|
||||||
else if (
|
|
||||||
new_ballPosY < 0 ||
|
|
||||||
new_ballPosY + BALL_SIZE * 2 > BOTTOM_EDGE
|
|
||||||
) {
|
|
||||||
ballSpeedY *= -1;
|
|
||||||
ballSpeedX *= ACCELERATION_FACTOR;
|
|
||||||
ballSpeedY *= ACCELERATION_FACTOR;
|
|
||||||
}
|
|
||||||
ballSpeedX = Math.max(
|
|
||||||
-ABS_MAX_BALL_SPEED,
|
|
||||||
Math.min(ballSpeedX, ABS_MAX_BALL_SPEED),
|
|
||||||
);
|
|
||||||
ballSpeedY = Math.max(
|
|
||||||
-ABS_MAX_BALL_SPEED,
|
|
||||||
Math.min(ballSpeedY, ABS_MAX_BALL_SPEED),
|
|
||||||
);
|
|
||||||
|
|
||||||
ballPosX += ballSpeedX;
|
|
||||||
ballPosY += ballSpeedY;
|
|
||||||
|
|
||||||
socket.emit('ballPos_update', ballPosX, ballPosY);
|
|
||||||
}, 16);
|
|
||||||
|
|
||||||
// QUEUE HANDL
|
|
||||||
socket.on('queuJoin', async (uuid: UserId) => {
|
|
||||||
console.log('queu join recieved for : ', uuid);
|
|
||||||
if (!(uuid in games.hasOwnProperty)) {
|
|
||||||
console.log('new user in game search queu');
|
|
||||||
games[uuid] = '';
|
|
||||||
}
|
|
||||||
else if (uuid in games && games[uuid] == '') {
|
|
||||||
console.log('already searching for game');
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// (games.hasOwnProperty(uuid) && games[uuid] != "") {
|
|
||||||
console.log('user alredy in game');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// TODO: step2 : sesrch in record<> find guid w/ "" &/ pair them up
|
|
||||||
// TODO: step3 : move game logic to lifecycle of queu'ed game
|
|
||||||
});
|
|
||||||
|
|
||||||
// other:
|
|
||||||
socket.on('message', (message: string) => {
|
|
||||||
const obj: ClientMessage = JSON.parse(message) as ClientMessage;
|
|
||||||
clientChat.set(socket.id, { user: obj.user, lastSeen: Date.now() });
|
|
||||||
socket.emit('welcome', { msg: 'Welcome to the chat! : ' });
|
|
||||||
broadcast(fastify, obj, obj.SenderWindowID);
|
|
||||||
});
|
|
||||||
socket.on('inviteGame', async (data: string) => {
|
|
||||||
const clientName: string = clientChat.get(socket.id)?.user || '';
|
|
||||||
const profilInvite: ClientProfil = JSON.parse(data) || '';
|
|
||||||
const inviteHtml: string =
|
|
||||||
'invites you to a game ' + setGameLink('');
|
|
||||||
if (clientName !== null) {
|
|
||||||
sendInvite(fastify, inviteHtml, profilInvite);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
import type { ClientMessage } from './chat_types';
|
|
||||||
import { clientChat, color } from './app';
|
|
||||||
import { FastifyInstance } from 'fastify';
|
|
||||||
|
|
||||||
export function broadcast(fastify: FastifyInstance, data: ClientMessage, sender?: string) {
|
|
||||||
fastify.io.fetchSockets().then((sockets) => {
|
|
||||||
for (const socket of sockets) {
|
|
||||||
// Skip sender's own socket
|
|
||||||
if (socket.id === sender) continue;
|
|
||||||
// Get client name from map
|
|
||||||
const clientInfo = clientChat.get(socket.id);
|
|
||||||
if (!clientInfo?.user) {
|
|
||||||
console.log(color.yellow, `Skipping socket ${socket.id} (no user found)`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// Emit structured JSON object
|
|
||||||
socket.emit('MsgObjectServer', { message: data });
|
|
||||||
// Debug logs
|
|
||||||
// console.log(color.green, `'DEBUG LOG: Broadcast to:', ${data.command} message: ${data.text}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
export type ClientMessage = {
|
|
||||||
command: string
|
|
||||||
destination: string;
|
|
||||||
user: string;
|
|
||||||
text: string;
|
|
||||||
SenderWindowID: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ClientProfil = {
|
|
||||||
command: string,
|
|
||||||
destination: string,
|
|
||||||
type: string,
|
|
||||||
user: string,
|
|
||||||
loginName: string,
|
|
||||||
userID: string,
|
|
||||||
text: string,
|
|
||||||
timestamp: number,
|
|
||||||
SenderWindowID: string,
|
|
||||||
SenderName: string,
|
|
||||||
Sendertext: string,
|
|
||||||
innerHtml?: string,
|
|
||||||
|
|
||||||
};
|
|
||||||
216
src/pong/src/game.ts
Normal file
216
src/pong/src/game.ts
Normal file
|
|
@ -0,0 +1,216 @@
|
||||||
|
import { UserId } from '@shared/database/mixin/user';
|
||||||
|
|
||||||
|
export class Paddle {
|
||||||
|
public static readonly DEFAULT_SPEED = 20;
|
||||||
|
public static readonly DEFAULT_HEIGHT = 80;
|
||||||
|
public static readonly DEFAULT_WIDTH = 12;
|
||||||
|
|
||||||
|
public height: number = Paddle.DEFAULT_HEIGHT;
|
||||||
|
public width: number = Paddle.DEFAULT_WIDTH;
|
||||||
|
public speed: number = Paddle.DEFAULT_SPEED;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
// these coordiantes are the topleft corordinates
|
||||||
|
public x: number,
|
||||||
|
public y: number,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
public move(dir: 'up' | 'down') {
|
||||||
|
this.y += (dir === 'up' ? -1 : 1) * this.speed;
|
||||||
|
}
|
||||||
|
|
||||||
|
public clamp(bottom: number, top: number) {
|
||||||
|
if (this.y <= bottom) this.y = bottom;
|
||||||
|
if (this.y + this.height >= top) this.y = top - this.height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Ball {
|
||||||
|
public static readonly DEFAULT_SPEED = 1;
|
||||||
|
public static readonly DEFAULT_SIZE = 16;
|
||||||
|
public static readonly DEFAULT_MAX_SPEED = 30;
|
||||||
|
public static readonly DEFAULT_MIN_SPEED = Ball.DEFAULT_SPEED;
|
||||||
|
public static readonly DEFAULT_ACCEL_FACTOR = 1.2;
|
||||||
|
|
||||||
|
public speed: number = Ball.DEFAULT_SPEED;
|
||||||
|
public size: number = Ball.DEFAULT_SIZE;
|
||||||
|
public accel_factor: number = Ball.DEFAULT_ACCEL_FACTOR;
|
||||||
|
|
||||||
|
public max_speed: number = Ball.DEFAULT_MAX_SPEED;
|
||||||
|
public min_speed: number = Ball.DEFAULT_MIN_SPEED;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
// these coordiantes are the center coordinates
|
||||||
|
public x: number,
|
||||||
|
public y: number,
|
||||||
|
public angle: number,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
public collided(
|
||||||
|
side: 'left' | 'right' | 'top' | 'bottom',
|
||||||
|
walls: { [k in typeof side]: number },
|
||||||
|
) {
|
||||||
|
// this.speed *= this.accel_factor;
|
||||||
|
this.speed = Math.max(
|
||||||
|
Math.min(this.speed, this.max_speed),
|
||||||
|
this.min_speed,
|
||||||
|
);
|
||||||
|
|
||||||
|
let c: 'x' | 'y' = 'x';
|
||||||
|
if (side === 'top' || side === 'bottom') {
|
||||||
|
this.angle = -this.angle;
|
||||||
|
c = 'y';
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.angle = -this.angle + Math.PI;
|
||||||
|
c = 'x';
|
||||||
|
}
|
||||||
|
this[c] =
|
||||||
|
walls[side] +
|
||||||
|
this.size * (side === 'right' || side === 'bottom' ? -1 : 1);
|
||||||
|
|
||||||
|
while (this.angle >= Math.PI) {
|
||||||
|
this.angle -= 2 * Math.PI;
|
||||||
|
}
|
||||||
|
while (this.angle < -Math.PI) {
|
||||||
|
this.angle += 2 * Math.PI;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public tick() {
|
||||||
|
this.x += Math.cos(this.angle) * this.speed;
|
||||||
|
this.y += Math.sin(this.angle) * this.speed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeAngle(i: number): [number, number, number, number] {
|
||||||
|
return [
|
||||||
|
Math.PI / i,
|
||||||
|
Math.PI / i + Math.PI,
|
||||||
|
-Math.PI / i,
|
||||||
|
-Math.PI / i + Math.PI,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Pong {
|
||||||
|
public gameUpdate: NodeJS.Timeout | null = null;
|
||||||
|
|
||||||
|
|
||||||
|
public static readonly BALL_START_ANGLES: number[] = [
|
||||||
|
...makeAngle(4),
|
||||||
|
...makeAngle(6),
|
||||||
|
];
|
||||||
|
|
||||||
|
public ballAngleIdx: number = 0;
|
||||||
|
|
||||||
|
public static readonly GAME_WIDTH: number = 800;
|
||||||
|
public static readonly GAME_HEIGHT: number = 450;
|
||||||
|
|
||||||
|
public static readonly PADDLE_OFFSET: number = 40;
|
||||||
|
|
||||||
|
public leftPaddle: Paddle = new Paddle(
|
||||||
|
Pong.PADDLE_OFFSET,
|
||||||
|
(Pong.GAME_HEIGHT - Paddle.DEFAULT_HEIGHT) / 2,
|
||||||
|
);
|
||||||
|
public rightPaddle: Paddle = new Paddle(
|
||||||
|
Pong.GAME_WIDTH - Pong.PADDLE_OFFSET - Paddle.DEFAULT_WIDTH,
|
||||||
|
(Pong.GAME_HEIGHT - Paddle.DEFAULT_HEIGHT) / 2,
|
||||||
|
);
|
||||||
|
public ball: Ball = new Ball(Pong.GAME_WIDTH / 2, Pong.GAME_HEIGHT / 2, Pong.BALL_START_ANGLES[this.ballAngleIdx++]);
|
||||||
|
|
||||||
|
public score: [number, number] = [0, 0];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public userLeft: UserId,
|
||||||
|
public userRight: UserId,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public tick() {
|
||||||
|
if (this.paddleCollision(this.leftPaddle, 'left')) {
|
||||||
|
this.ball.collided('left', {
|
||||||
|
left: this.leftPaddle.x + this.leftPaddle.width,
|
||||||
|
right: 0,
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.paddleCollision(this.rightPaddle, 'right')) {
|
||||||
|
this.ball.collided('right', {
|
||||||
|
right: this.rightPaddle.x,
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const wallCollision = this.boxCollision();
|
||||||
|
if (wallCollision === 'top' || wallCollision === 'bottom') {
|
||||||
|
this.ball.collided(wallCollision, {
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
bottom: Pong.GAME_HEIGHT,
|
||||||
|
right: Pong.GAME_WIDTH,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else if (wallCollision !== null) {
|
||||||
|
const idx = wallCollision === 'left' ? 1 : 0;
|
||||||
|
this.score[idx] += 1;
|
||||||
|
this.ball = new Ball(
|
||||||
|
Pong.GAME_WIDTH / 2,
|
||||||
|
Pong.GAME_HEIGHT / 2,
|
||||||
|
Pong.BALL_START_ANGLES[this.ballAngleIdx++],
|
||||||
|
);
|
||||||
|
this.ballAngleIdx %= Pong.BALL_START_ANGLES.length;
|
||||||
|
}
|
||||||
|
this.ball.tick();
|
||||||
|
}
|
||||||
|
|
||||||
|
// This function will return which side the ball collided, if any
|
||||||
|
private boxCollision(): 'top' | 'bottom' | 'left' | 'right' | null {
|
||||||
|
if (this.ball.y - this.ball.size <= 0) return 'top';
|
||||||
|
if (this.ball.y + this.ball.size >= Pong.GAME_HEIGHT) return 'bottom';
|
||||||
|
if (this.ball.x - this.ball.size <= 0) return 'left';
|
||||||
|
if (this.ball.x + this.ball.size >= Pong.GAME_WIDTH) return 'right';
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private paddleCollision(paddle: Paddle, side: 'left' | 'right'): boolean {
|
||||||
|
// now we check only if the ball is near enought in the y axis to permform the collision
|
||||||
|
if (!(
|
||||||
|
// check if ball is bellow the top of the paddle
|
||||||
|
paddle.y - this.ball.size < this.ball.y &&
|
||||||
|
// check if ball is above the bottom of the paddle
|
||||||
|
this.ball.y < paddle.y + paddle.height + this.ball.size)) return false;
|
||||||
|
|
||||||
|
// so we know that the y is close enougth to be a bit, so we check the X. are we closer than the ball size ? if yes -> hit
|
||||||
|
if (
|
||||||
|
// check if the paddle.x is at most ball.size away from the center of the ball => we have a hit houston
|
||||||
|
// call he pentagon, 9 11
|
||||||
|
Math.abs(
|
||||||
|
paddle.x + paddle.width * (side === 'left' ? 1 : 0)
|
||||||
|
- this.ball.x)
|
||||||
|
< this.ball.size
|
||||||
|
) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public checkWinner(): 'left' | 'right' | null {
|
||||||
|
if (this.score[0] >= 5) return 'left';
|
||||||
|
if (this.score[1] >= 5) return 'right';
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public movePaddle(user: UserId, dir: 'up' | 'down') {
|
||||||
|
const paddle =
|
||||||
|
user === this.userLeft
|
||||||
|
? this.leftPaddle
|
||||||
|
: user == this.userRight
|
||||||
|
? this.rightPaddle
|
||||||
|
: null;
|
||||||
|
if (paddle === null) return;
|
||||||
|
paddle.move(dir);
|
||||||
|
paddle.clamp(0, Pong.GAME_HEIGHT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,7 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
import { JwtType } from '@shared/auth';
|
||||||
|
import { UserId } from '@shared/database/mixin/user';
|
||||||
|
import { isNullish } from '@shared/utils';
|
||||||
import type {
|
import type {
|
||||||
FastifyInstance,
|
FastifyInstance,
|
||||||
FastifyPluginAsync,
|
FastifyPluginAsync,
|
||||||
|
|
@ -11,6 +15,21 @@ const F: (
|
||||||
) => Omit<FastifyInstance, 'io'> & { io: Server } = (f) =>
|
) => Omit<FastifyInstance, 'io'> & { io: Server } = (f) =>
|
||||||
f as Omit<FastifyInstance, 'io'> & { io: Server };
|
f as Omit<FastifyInstance, 'io'> & { io: Server };
|
||||||
|
|
||||||
|
function authenticateToken(
|
||||||
|
fastify: FastifyInstance,
|
||||||
|
token: string,
|
||||||
|
): { id: UserId; name: string; guest: boolean } {
|
||||||
|
const tok = fastify.jwt.verify<JwtType>(token);
|
||||||
|
if (tok.kind != 'auth') {
|
||||||
|
throw new Error('Token isn\'t correct type');
|
||||||
|
}
|
||||||
|
const user = fastify.db.getUser(tok.who);
|
||||||
|
if (isNullish(user)) {
|
||||||
|
throw new Error('User not found');
|
||||||
|
}
|
||||||
|
return { id: user.id, name: user.name, guest: user.guest };
|
||||||
|
}
|
||||||
|
|
||||||
const fastifySocketIO: FastifyPluginAsync = fp(async (fastify) => {
|
const fastifySocketIO: FastifyPluginAsync = fp(async (fastify) => {
|
||||||
function defaultPreClose(done: HookHandlerDoneFunction) {
|
function defaultPreClose(done: HookHandlerDoneFunction) {
|
||||||
F(fastify).io.local.disconnectSockets(true);
|
F(fastify).io.local.disconnectSockets(true);
|
||||||
|
|
@ -20,6 +39,36 @@ const fastifySocketIO: FastifyPluginAsync = fp(async (fastify) => {
|
||||||
'io',
|
'io',
|
||||||
new Server(fastify.server, { path: '/api/pong/socket.io' }),
|
new Server(fastify.server, { path: '/api/pong/socket.io' }),
|
||||||
);
|
);
|
||||||
|
F(fastify).io.use((socket, next) => {
|
||||||
|
const cookieHeader = socket.request.headers.cookie;
|
||||||
|
|
||||||
|
if (!cookieHeader) {
|
||||||
|
throw new Error('Missing token cookie');
|
||||||
|
}
|
||||||
|
|
||||||
|
const cookies = Object.fromEntries(
|
||||||
|
cookieHeader.split(';').map((c) => {
|
||||||
|
const [k, v] = c.trim().split('=');
|
||||||
|
return [k, v];
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!cookies.token) {
|
||||||
|
throw new Error('Missing token cookie');
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
socket.authUser = authenticateToken(fastify, cookies.token);
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
catch (e: any) {
|
||||||
|
next({
|
||||||
|
name: 'Unauthorized',
|
||||||
|
message: e.message,
|
||||||
|
data: { status: 401 },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
fastify.addHook('preClose', defaultPreClose);
|
fastify.addHook('preClose', defaultPreClose);
|
||||||
fastify.addHook('onClose', (instance: FastifyInstance, done) => {
|
fastify.addHook('onClose', (instance: FastifyInstance, done) => {
|
||||||
F(instance).io.close();
|
F(instance).io.close();
|
||||||
|
|
@ -28,4 +77,3 @@ const fastifySocketIO: FastifyPluginAsync = fp(async (fastify) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
export default fastifySocketIO;
|
export default fastifySocketIO;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { FastifyPluginAsync } from 'fastify';
|
import { FastifyPluginAsync } from 'fastify';
|
||||||
import { Static, Type } from 'typebox';
|
import { Static, Type } from 'typebox';
|
||||||
import { broadcast } from '../broadcast';
|
|
||||||
|
|
||||||
export const PongReq = Type.Object({
|
export const PongReq = Type.Object({
|
||||||
message: Type.String(),
|
message: Type.String(),
|
||||||
|
|
@ -19,7 +18,6 @@ const route: FastifyPluginAsync = async (fastify): Promise<void> => {
|
||||||
config: { requireAuth: false },
|
config: { requireAuth: false },
|
||||||
},
|
},
|
||||||
async function(req, res) {
|
async function(req, res) {
|
||||||
broadcast(this, { command: '', destination: 'system-info', user: 'CMwaLeSever!!', text: req.body.message, SenderWindowID: 'server' });
|
|
||||||
void res;
|
void res;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
import type { ClientProfil } from './chat_types';
|
|
||||||
import { clientChat, color } from './app';
|
|
||||||
import { FastifyInstance } from 'fastify';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* function looks for the user online in the chat
|
|
||||||
* and sends emit to invite - format HTML to make clickable
|
|
||||||
* message appears in chat window text area
|
|
||||||
* @param fastify
|
|
||||||
* @param innerHtml
|
|
||||||
* @param profil
|
|
||||||
*/
|
|
||||||
|
|
||||||
export function sendInvite(fastify: FastifyInstance, innerHtml: string, profil: ClientProfil) {
|
|
||||||
fastify.io.fetchSockets().then((sockets) => {
|
|
||||||
let targetSocket;
|
|
||||||
for (const socket of sockets) {
|
|
||||||
const clientInfo: string = clientChat.get(socket.id)?.user || '';
|
|
||||||
if (clientInfo === profil.user) {
|
|
||||||
console.log(color.yellow, 'DEBUG LOG: user online found', profil.user);
|
|
||||||
targetSocket = socket || '';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
profil.innerHtml = innerHtml ?? '';
|
|
||||||
if (targetSocket) {
|
|
||||||
targetSocket.emit('inviteGame', profil);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
export function setGameLink(link: string): string {
|
|
||||||
if (!link) {
|
|
||||||
link = '<a href=\'https://google.com\' style=\'color: blue; text-decoration: underline; cursor: pointer;\'>Click me</a>';
|
|
||||||
}
|
|
||||||
return link;
|
|
||||||
};
|
|
||||||
47
src/pong/src/socket.ts
Normal file
47
src/pong/src/socket.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
import { Socket } from 'socket.io';
|
||||||
|
|
||||||
|
export type UpdateInfo = {
|
||||||
|
inQueue: number,
|
||||||
|
totalUser: number,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PaddleData = {
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GameUpdate = {
|
||||||
|
gameId: string;
|
||||||
|
|
||||||
|
left: { id: string, paddle: PaddleData, score: number };
|
||||||
|
right: { id: string, paddle: PaddleData, score: number };
|
||||||
|
|
||||||
|
ball: { x: number, y: number, size: number };
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GameMove = {
|
||||||
|
move: 'up' | 'down' | null,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ClientToServer {
|
||||||
|
enqueue: () => void;
|
||||||
|
dequeue: () => void;
|
||||||
|
debugInfo: () => void;
|
||||||
|
gameMove: (up: GameMove) => void;
|
||||||
|
connectedToGame: (gameId: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface ServerToClient {
|
||||||
|
forceDisconnect: (reason: string) => void;
|
||||||
|
queueEvent: (msg: 'registered' | 'unregistered') => void;
|
||||||
|
updateInformation: (info: UpdateInfo) => void,
|
||||||
|
newGame: (initState: GameUpdate) => void,
|
||||||
|
gameUpdate: (state: GameUpdate) => void,
|
||||||
|
gameEnd: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SSocket = Socket<ClientToServer, ServerToClient>;
|
||||||
|
export type CSocket = Socket<ServerToClient, ClientToServer>;
|
||||||
182
src/pong/src/state.ts
Normal file
182
src/pong/src/state.ts
Normal file
|
|
@ -0,0 +1,182 @@
|
||||||
|
import { UserId } from '@shared/database/mixin/user';
|
||||||
|
import { newUUID } from '@shared/utils/uuid';
|
||||||
|
import { FastifyInstance } from 'fastify';
|
||||||
|
import { Pong } from './game';
|
||||||
|
import { GameMove, GameUpdate, SSocket } from './socket';
|
||||||
|
import { isNullish } from '@shared/utils';
|
||||||
|
|
||||||
|
type PUser = {
|
||||||
|
id: UserId;
|
||||||
|
currentGame: null | GameId;
|
||||||
|
socket: SSocket,
|
||||||
|
windowId: string,
|
||||||
|
updateInterval: NodeJS.Timeout,
|
||||||
|
};
|
||||||
|
|
||||||
|
type GameId = string & { readonly __brand: unique symbol };
|
||||||
|
|
||||||
|
class StateI {
|
||||||
|
public static readonly UPDATE_INTERVAL_FRAMES: number = 60;
|
||||||
|
|
||||||
|
private users: Map<UserId, PUser> = new Map();
|
||||||
|
private queue: Set<UserId> = new Set();
|
||||||
|
private queueInterval: NodeJS.Timeout;
|
||||||
|
private games: Map<GameId, Pong> = new Map();
|
||||||
|
|
||||||
|
public constructor(private fastify: FastifyInstance) {
|
||||||
|
this.queueInterval = setInterval(() => this.queuerFunction());
|
||||||
|
void this.queueInterval;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static getGameUpdateData(id: GameId, g: Pong): GameUpdate {
|
||||||
|
return {
|
||||||
|
gameId: id,
|
||||||
|
left: { id: g.userLeft, score: g.score[0], paddle: { x: g.leftPaddle.x, y: g.leftPaddle.y, width: g.leftPaddle.width, height: g.leftPaddle.height } },
|
||||||
|
right: { id: g.userRight, score: g.score[1], paddle: { x: g.rightPaddle.x, y: g.rightPaddle.y, width: g.rightPaddle.width, height: g.rightPaddle.height } },
|
||||||
|
ball: { x: g.ball.x, y: g.ball.y, size: g.ball.size },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private queuerFunction(): void {
|
||||||
|
const values = Array.from(this.queue.values());
|
||||||
|
while (values.length >= 2) {
|
||||||
|
const id1 = values.pop();
|
||||||
|
const id2 = values.pop();
|
||||||
|
|
||||||
|
if (isNullish(id1) || isNullish(id2)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const u1 = this.users.get(id1);
|
||||||
|
const u2 = this.users.get(id2);
|
||||||
|
|
||||||
|
if (isNullish(u1) || isNullish(u2)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
this.queue.delete(id1);
|
||||||
|
this.queue.delete(id2);
|
||||||
|
|
||||||
|
const gameId = newUUID() as unknown as GameId;
|
||||||
|
const g = new Pong(u1.id, u2.id);
|
||||||
|
const iState: GameUpdate = StateI.getGameUpdateData(gameId, g);
|
||||||
|
|
||||||
|
u1.socket.emit('newGame', iState);
|
||||||
|
u2.socket.emit('newGame', iState);
|
||||||
|
this.games.set(gameId, g);
|
||||||
|
|
||||||
|
u1.currentGame = gameId;
|
||||||
|
u2.currentGame = gameId;
|
||||||
|
|
||||||
|
g.gameUpdate = setInterval(() => {
|
||||||
|
g.tick();
|
||||||
|
this.gameUpdate(gameId, u1.socket);
|
||||||
|
this.gameUpdate(gameId, u2.socket);
|
||||||
|
if (g.checkWinner() !== null) { this.cleanupGame(gameId, g); }
|
||||||
|
}, 1000 / StateI.UPDATE_INTERVAL_FRAMES);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private gameUpdate(id: GameId, sock: SSocket) {
|
||||||
|
// does the game we want to update the client exists ?
|
||||||
|
if (!this.games.has(id)) return;
|
||||||
|
// is the client someone we know ?
|
||||||
|
if (!this.users.has(sock.authUser.id)) return;
|
||||||
|
// is the client associated with that game ?
|
||||||
|
if (this.users.get(sock.authUser.id)!.currentGame !== id) return;
|
||||||
|
sock.emit('gameUpdate', StateI.getGameUpdateData(id, this.games.get(id)!));
|
||||||
|
}
|
||||||
|
|
||||||
|
private gameMove(socket: SSocket, u: GameMove) {
|
||||||
|
// do we know this user ?
|
||||||
|
if (!this.users.has(socket.authUser.id)) return;
|
||||||
|
const user = this.users.get(socket.authUser.id)!;
|
||||||
|
// does the user have a game and do we know such game ?
|
||||||
|
if (user.currentGame === null || !this.games.has(user.currentGame)) return;
|
||||||
|
const game = this.games.get(user.currentGame)!;
|
||||||
|
|
||||||
|
if (u.move !== null) { game.movePaddle(user.id, u.move); }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public registerUser(socket: SSocket): void {
|
||||||
|
this.fastify.log.info('Registering new user');
|
||||||
|
if (this.users.has(socket.authUser.id)) {
|
||||||
|
socket.emit('forceDisconnect', 'Already Connected');
|
||||||
|
socket.disconnect();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.users.set(socket.authUser.id, {
|
||||||
|
socket,
|
||||||
|
id: socket.authUser.id,
|
||||||
|
windowId: socket.id,
|
||||||
|
updateInterval: setInterval(() => this.updateClient(socket), 3000),
|
||||||
|
currentGame: null,
|
||||||
|
});
|
||||||
|
this.fastify.log.info('Registered new user');
|
||||||
|
|
||||||
|
socket.on('disconnect', () => this.cleanupUser(socket));
|
||||||
|
socket.on('enqueue', () => this.enqueueUser(socket));
|
||||||
|
socket.on('dequeue', () => this.dequeueUser(socket));
|
||||||
|
|
||||||
|
socket.on('gameMove', (e) => this.gameMove(socket, e));
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateClient(socket: SSocket): void {
|
||||||
|
socket.emit('updateInformation', {
|
||||||
|
inQueue: this.queue.size,
|
||||||
|
totalUser: this.users.size,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private cleanupUser(socket: SSocket): void {
|
||||||
|
if (!this.users.has(socket.authUser.id)) return;
|
||||||
|
|
||||||
|
clearInterval(this.users.get(socket.authUser.id)?.updateInterval);
|
||||||
|
this.users.delete(socket.authUser.id);
|
||||||
|
this.queue.delete(socket.authUser.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private cleanupGame(gameId: GameId, game: Pong): void {
|
||||||
|
clearInterval(game.gameUpdate ?? undefined);
|
||||||
|
this.games.delete(gameId);
|
||||||
|
let player: PUser | undefined = undefined;
|
||||||
|
if ((player = this.users.get(game.userLeft)) !== undefined) {
|
||||||
|
player.currentGame = null;
|
||||||
|
player.socket.emit('gameEnd');
|
||||||
|
}
|
||||||
|
if ((player = this.users.get(game.userRight)) !== undefined) {
|
||||||
|
player.currentGame = null;
|
||||||
|
player.socket.emit('gameEnd');
|
||||||
|
}
|
||||||
|
// do something here with the game result before deleting the game at the end
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private enqueueUser(socket: SSocket): void {
|
||||||
|
if (!this.users.has(socket.authUser.id)) return;
|
||||||
|
|
||||||
|
if (this.queue.has(socket.authUser.id)) return;
|
||||||
|
|
||||||
|
if (this.users.get(socket.authUser.id)?.currentGame !== null) return;
|
||||||
|
|
||||||
|
this.queue.add(socket.authUser.id);
|
||||||
|
socket.emit('queueEvent', 'registered');
|
||||||
|
}
|
||||||
|
|
||||||
|
private dequeueUser(socket: SSocket): void {
|
||||||
|
if (!this.users.has(socket.authUser.id)) return;
|
||||||
|
|
||||||
|
if (!this.queue.has(socket.authUser.id)) return;
|
||||||
|
|
||||||
|
this.queue.delete(socket.authUser.id);
|
||||||
|
socket.emit('queueEvent', 'unregistered');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export let State: StateI = undefined as unknown as StateI;
|
||||||
|
|
||||||
|
export function newState(f: FastifyInstance) {
|
||||||
|
State = new StateI(f);
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue