From 2509199abccb2bdb7f9c8ff8b01df8452758b2e4 Mon Sep 17 00:00:00 2001 From: schmelczerandras Date: Wed, 21 Oct 2020 20:27:03 +0200 Subject: [PATCH] Add landing page player count updates --- backend/src/game-server.ts | 17 ++- frontend/src/scripts/config/configuration.ts | 4 + frontend/src/scripts/join-form-handler.ts | 110 +++++++++++++++---- shared/src/transport/transport-events.ts | 2 + 4 files changed, 111 insertions(+), 22 deletions(-) diff --git a/backend/src/game-server.ts b/backend/src/game-server.ts index 93446a0..991069b 100644 --- a/backend/src/game-server.ts +++ b/backend/src/game-server.ts @@ -12,6 +12,8 @@ import { createWorld } from './map/create-world'; import { DeltaTimeCalculator } from './helper/delta-time-calculator'; import { Options } from './options'; +const playerCountSubscribedRoom = 'playerCountUpdates'; + export class GameServer { private objects = new PhysicalContainer(); private players: Array = []; @@ -20,7 +22,7 @@ export class GameServer { private serverName: string; private playerLimit: number; - constructor(io: ioserver.Server, options: Options) { + constructor(private readonly io: ioserver.Server, options: Options) { this.serverName = options.name; this.playerLimit = options.playerLimit; @@ -36,18 +38,27 @@ export class GameServer { player.sendCommand(command); }); + this.sendPlayerCountUpdate(); + socket.on('disconnect', () => { player.destroy(); this.players = this.players.filter((p) => p !== player); + this.sendPlayerCountUpdate(); }); }); - socket.on('join', (room_name: string) => { - socket.join(room_name); + socket.on(TransportEvents.SubscribeForPlayerCount, () => { + socket.join(playerCountSubscribedRoom); }); }); } + public sendPlayerCountUpdate() { + this.io + .to(playerCountSubscribedRoom) + .emit(TransportEvents.PlayerCountUpdate, this.players.length); + } + public start() { this.handlePhysics(); } diff --git a/frontend/src/scripts/config/configuration.ts b/frontend/src/scripts/config/configuration.ts index fedb2e5..49e2017 100644 --- a/frontend/src/scripts/config/configuration.ts +++ b/frontend/src/scripts/config/configuration.ts @@ -6,6 +6,10 @@ export abstract class Configuration { private static initialized = false; public static async initialize(): Promise { + if (this.initialized) { + return; + } + const firebaseConfig = { apiKey: 'AIzaSyBG85dp-AhaCW-qi_6mu77wDPSipzipIF4', authDomain: 'decla-red.firebaseapp.com', diff --git a/frontend/src/scripts/join-form-handler.ts b/frontend/src/scripts/join-form-handler.ts index 3d9e149..2fa9ba5 100644 --- a/frontend/src/scripts/join-form-handler.ts +++ b/frontend/src/scripts/join-form-handler.ts @@ -1,5 +1,5 @@ -import { ServerInformation, serverInformationEndpoint } from 'shared'; - +import { ServerInformation, serverInformationEndpoint, TransportEvents } from 'shared'; +import io from 'socket.io-client'; import { Configuration } from './config/configuration'; export type PlayerDecision = { @@ -7,9 +7,11 @@ export type PlayerDecision = { server: string; }; +const pollInterval = 10000; export class JoinFormHandler { private waitingForDecision: Promise; private resolvePlayerDecision!: (d: PlayerDecision) => void; + private pollServersTimer: any; constructor(form: HTMLFormElement, private readonly container: HTMLElement) { this.waitingForDecision = new Promise((r) => (this.resolvePlayerDecision = r)); @@ -35,18 +37,51 @@ export class JoinFormHandler { e.preventDefault(); }; + this.pollServersTimer = setInterval(this.loadServers.bind(this), pollInterval); this.loadServers(); + this.waitForFinish(); } + private async waitForFinish() { + await this.waitingForDecision; + this.destroy(); + } + + private destroy() { + clearInterval(this.pollServersTimer); + this.servers.forEach((s) => s.destroy()); + } + + private servers: Array = []; private async loadServers() { await Configuration.initialize(); - const serverList = Configuration.servers; + + const serverList = Configuration.servers.filter( + (u) => !this.servers.find((s) => s.url === u), + ); serverList.map(async (url) => { - const response = await fetch(url + serverInformationEndpoint); - if (response.ok) { + const controller = new AbortController(); + const signal = controller.signal; + setTimeout(() => controller.abort(), pollInterval * 0.8); + + let response: Response | undefined; + try { + response = await fetch(url + serverInformationEndpoint, { signal }); + } catch { + // it's okay + } + + if (response?.ok) { const content: ServerInformation = await response.json(); - this.displayNewServerInfo(content, url); + const server = new ServerChooserOption( + content, + url, + (r) => (this.servers = this.servers.filter((s) => s !== r)), + this.servers.length === 0, + ); + this.servers.push(server); + this.container.appendChild(server.element); } }); } @@ -54,19 +89,56 @@ export class JoinFormHandler { public async getPlayerDecision(): Promise { return this.waitingForDecision; } +} - private isFirstServer = true; - private displayNewServerInfo(content: ServerInformation, url: string) { - this.container.innerHTML += ` -
- - -
- `; - this.isFirstServer = false; +class ServerChooserOption { + private divElement = document.createElement('div'); + private inputElement = document.createElement('input'); + private labelElement = document.createElement('label'); + private socket: SocketIOClient.Socket; + + constructor( + private content: ServerInformation, + public readonly url: string, + private onDestroy: (v: ServerChooserOption) => unknown, + isFirst: boolean, + ) { + this.inputElement.required = true; + this.inputElement.type = 'radio'; + this.inputElement.id = this.inputElement.value = url; + this.inputElement.name = 'server'; + this.inputElement.checked = isFirst; + this.labelElement.htmlFor = url; + this.divElement.appendChild(this.inputElement); + this.divElement.appendChild(this.labelElement); + this.setPlayerLabelText(); + + this.socket = io(url, { + reconnection: false, + timeout: 1500, + }); + + this.socket.on('connect_error', this.destroy.bind(this)); + this.socket.on('connect_timeout', this.destroy.bind(this)); + this.socket.on('disconnect', this.destroy.bind(this)); + this.socket.emit(TransportEvents.SubscribeForPlayerCount); + this.socket.on(TransportEvents.PlayerCountUpdate, (v: number) => { + this.content.playerCount = v; + this.setPlayerLabelText(); + }); + } + + public destroy() { + this.socket.close(); + this.divElement.parentElement?.removeChild(this.divElement); + this.onDestroy(this); + } + + private setPlayerLabelText() { + this.labelElement.innerText = `${this.content.serverName} - ${this.content.playerCount}/${this.content.playerLimit} players`; + } + + public get element(): HTMLElement { + return this.divElement; } } diff --git a/shared/src/transport/transport-events.ts b/shared/src/transport/transport-events.ts index b254201..8104af6 100644 --- a/shared/src/transport/transport-events.ts +++ b/shared/src/transport/transport-events.ts @@ -2,6 +2,8 @@ export enum TransportEvents { PlayerJoining = 'PlayerJoining', PlayerToServer = 'PlayerToServer', ServerToPlayer = 'ServerToPlayer', + SubscribeForPlayerCount = 'SubscribeForPlayerCount', + PlayerCountUpdate = 'PlayerCountUpdate', Ping = 'Ping', Pong = 'Pong', }