From d92dc9eba644f0b3206afe9d6300144e27fc9fe0 Mon Sep 17 00:00:00 2001 From: evanpelle Date: Wed, 16 Oct 2024 19:52:05 -0700 Subject: [PATCH] rewrite game join network logic: don't load client game until game starts. Allows setting attrs like game map, difficulty --- src/client/ClientGame.ts | 115 ++++++++++++++++++------------ src/client/HostLobbyModal.ts | 2 +- src/client/Main.ts | 29 ++++---- src/client/Transport.ts | 56 ++++++++++----- src/core/game/TerrainMapLoader.ts | 10 ++- 5 files changed, 131 insertions(+), 81 deletions(-) diff --git a/src/client/ClientGame.ts b/src/client/ClientGame.ts index 7a4d754c8..52cfd3465 100644 --- a/src/client/ClientGame.ts +++ b/src/client/ClientGame.ts @@ -15,32 +15,64 @@ import {DisplayMessageEvent, MessageType} from "./graphics/layers/EventsDisplay" import {v4 as uuidv4} from 'uuid'; -export interface GameConfig { +export interface LobbyConfig { isLocal: boolean playerName: () => string gameID: GameID ip: string | null - map: GameMap + map: GameMap | null } -export async function createClientGame(gameConfig: GameConfig): Promise { - let eventBus = new EventBus() - const config = getConfig() +export interface GameConfig { + map: GameMap + clientID: ClientID, + gameID: GameID, + ip: string | null, +} +export function joinLobby(lobbyConfig: LobbyConfig, onjoin: () => void): () => void { const clientID = uuidv4() const playerID = uuidv4() + const eventBus = new EventBus() + const config = getConfig() + const transport = new Transport(lobbyConfig.isLocal, eventBus, lobbyConfig.gameID, clientID, playerID, config, lobbyConfig.playerName) + + const onconnect = () => { + console.log('Joined game lobby!'); + transport.joinGame(clientID, 0) + }; + const onmessage = (message: ServerMessage) => { + if (message.type == "start") { + console.log('lobby: game started') + onjoin() + const gameConfig = { + map: GameMap.World, + clientID: clientID, + gameID: lobbyConfig.gameID, + ip: lobbyConfig.ip, + } + createClientGame(gameConfig, eventBus, transport).then(r => r.start()) + }; + } + transport.connect(onconnect, onmessage) + return () => { + console.log('leaving game') + transport.leaveGame() + } +} + + +export async function createClientGame(gameConfig: GameConfig, eventBus: EventBus, transport: Transport): Promise { + const config = getConfig() const terrainMap = await loadTerrainMap(gameConfig.map) let game = createGame(terrainMap, eventBus, config) const canvas = createCanvas() - let gameRenderer = createRenderer(canvas, game, eventBus, clientID) + let gameRenderer = createRenderer(canvas, game, eventBus, gameConfig.clientID) - const transport = new Transport(gameConfig.isLocal, eventBus, gameConfig.gameID, clientID, playerID, config, gameConfig.playerName) - - - return new ClientGame( - clientID, + return new GameRunner( + gameConfig.clientID, gameConfig.ip, eventBus, game, @@ -51,7 +83,7 @@ export async function createClientGame(gameConfig: GameConfig): Promise void) { - const onconnect = () => { - console.log('Connected to game server!'); - this.transport.joinGame(this.clientIP, this.turns.length) - }; - const onmessage = (message: ServerMessage) => { - if (message.type == "start") { - console.log("starting game!") - onstart() - for (const turn of message.turns) { - if (turn.turnNumber < this.turns.length) { - continue - } - this.turns.push(turn) - } - if (!this.isActive) { - this.start() - } - } - if (message.type == "turn") { - this.addTurn(message.turn) - } - }; - this.transport.connect(onconnect, onmessage, () => this.isActive) - } - public start() { + console.log('starting client game') this.isActive = true this.eventBus.on(PlayerEvent, (e) => this.playerEvent(e)) this.eventBus.on(MouseUpEvent, (e) => this.inputEvent(e)) @@ -111,6 +118,31 @@ export class ClientGame { this.gs.addExecution(new WinCheckExecution(this.eventBus)) this.intervalID = setInterval(() => this.tick(), 10); + + const onconnect = () => { + console.log('Connected to game server!'); + this.transport.joinGame(this.clientIP, this.turns.length) + }; + const onmessage = (message: ServerMessage) => { + if (message.type == "start") { + console.log("starting game!") + for (const turn of message.turns) { + if (turn.turnNumber < this.turns.length) { + continue + } + this.turns.push(turn) + } + } + if (message.type == "turn") { + if (this.turns.length != message.turn.turnNumber) { + console.error(`got wrong turn have turns ${this.turns.length}, received turn ${message.turn.turnNumber}`) + } else { + this.turns.push(message.turn) + } + } + }; + this.transport.connect(onconnect, onmessage) + } public stop() { @@ -119,13 +151,6 @@ export class ClientGame { this.transport.leaveGame() } - public addTurn(turn: Turn): void { - if (this.turns.length != turn.turnNumber) { - console.error(`got wrong turn have turns ${this.turns.length}, received turn ${turn.turnNumber}`) - } - this.turns.push(turn) - } - public tick() { if (this.currTurn >= this.turns.length || this.isProcessingTurn) { return diff --git a/src/client/HostLobbyModal.ts b/src/client/HostLobbyModal.ts index c0ced4f10..893d07545 100644 --- a/src/client/HostLobbyModal.ts +++ b/src/client/HostLobbyModal.ts @@ -156,7 +156,7 @@ export class HostLobbyModal extends LitElement { this.selectedMap = Number((e.target as HTMLSelectElement).value) as GameMap; } private async startGame() { - console.log(`Starting single player game with map: ${GameMap[this.selectedMap]}`); + console.log(`Starting private game with map: ${GameMap[this.selectedMap]}`); this.close(); const response = await fetch(`/start_private_lobby/${this.lobbyId}`, { method: 'POST', diff --git a/src/client/Main.ts b/src/client/Main.ts index 2619e54cd..55f7eb6bf 100644 --- a/src/client/Main.ts +++ b/src/client/Main.ts @@ -1,4 +1,4 @@ -import {ClientGame, createClientGame} from "./ClientGame"; +import {GameRunner, joinLobby} from "./ClientGame"; import backgroundImage from '../../resources/images/TerrainMapFrontPage.png'; import favicon from '../../resources/images/Favicon.png'; @@ -14,7 +14,7 @@ import {JoinPrivateLobbyModal} from "./JoinPrivateLobbyModal"; class Client { - private game: ClientGame + private gameStop: () => void private ip: Promise = null @@ -59,8 +59,6 @@ class Client { document.getElementById('join-private-lobby-button').addEventListener('click', () => { this.joinModal.open(); }) - - } private async handleJoinLobby(event: CustomEvent) { @@ -68,38 +66,35 @@ class Client { console.log(`joining lobby ${lobby.id}`) const clientIP = await this.ip console.log(`got ip ${clientIP}`) - if (this.game != null) { + if (this.gameStop != null) { console.log('joining lobby, stopping existing game') - this.game.stop() + this.gameStop() } - this.game = await createClientGame( + this.gameStop = joinLobby( { isLocal: event.detail.singlePlayer, playerName: (): string => this.usernameInput.getCurrentUsername(), gameID: lobby.id, ip: clientIP, map: event.detail.map, - } + }, + () => this.joinModal.close() ); - this.game.join(() => { - this.joinModal.close() - }); - const g = this.game; } private stopGame() { - if (this.game != null) { - this.game.stop() + if (this.gameStop != null) { + this.gameStop() } } private async handleLeaveLobby(event: CustomEvent) { - if (this.game == null) { + if (this.gameStop == null) { return } console.log('leaving lobby, cancelling game') - this.game.stop() - this.game = null + this.gameStop() + this.gameStop = null } private async handleSinglePlayer(event: CustomEvent) { diff --git a/src/client/Transport.ts b/src/client/Transport.ts index 07b3b22f3..8ac43e989 100644 --- a/src/client/Transport.ts +++ b/src/client/Transport.ts @@ -73,6 +73,12 @@ export class Transport { private localServer: LocalServer + private buffer: string[] = [] + + + private onconnect: () => void + private onmessage: (msg: ServerMessage) => void + constructor( private isLocal: boolean, private eventBus: EventBus, @@ -93,25 +99,34 @@ export class Transport { this.eventBus.on(SendDonateIntentEvent, (e) => this.onSendDonateIntent(e)) } - connect(onconnect: () => void, onmessage: (message: ServerMessage) => void, isActive: () => boolean) { + connect(onconnect: () => void, onmessage: (message: ServerMessage) => void) { if (this.isLocal) { - this.connectLocal(onconnect, onmessage, isActive) + this.connectLocal(onconnect, onmessage) } else { - this.connectRemote(onconnect, onmessage, isActive) + this.connectRemote(onconnect, onmessage) } } - private connectLocal(onconnect: () => void, onmessage: (message: ServerMessage) => void, isActive: () => boolean) { + private connectLocal(onconnect: () => void, onmessage: (message: ServerMessage) => void) { this.localServer = new LocalServer(this.config, onconnect, onmessage) this.localServer.start() } - private connectRemote(onconnect: () => void, onmessage: (message: ServerMessage) => void, isActive: () => boolean) { - const wsHost = process.env.WEBSOCKET_URL || window.location.host; - const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; - this.socket = new WebSocket(`${wsProtocol}//${wsHost}`) + private connectRemote(onconnect: () => void, onmessage: (message: ServerMessage) => void) { + const isFirstConnect = this.socket == null + if (isFirstConnect) { + const wsHost = process.env.WEBSOCKET_URL || window.location.host; + const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; + this.socket = new WebSocket(`${wsProtocol}//${wsHost}`) + } + this.onconnect = onconnect + this.onmessage = onmessage this.socket.onopen = () => { console.log('Connected to game server!'); + while (this.buffer.length > 0) { + console.log('sending dropped message') + this.sendMsg(this.buffer.pop()) + } onconnect() }; this.socket.onmessage = (event: MessageEvent) => { @@ -123,13 +138,13 @@ export class Transport { }; this.socket.onclose = (event: CloseEvent) => { console.log(`WebSocket closed. Code: ${event.code}, Reason: ${event.reason}`); - if (event.code != 1000) { - console.log(`reconnecting`) - this.connect(onconnect, onmessage, isActive) - } else { - console.log('normal websocket closure') - } + console.log(`reconnecting`) + this.connect(onconnect, onmessage) }; + if (!isFirstConnect) { + // Socket has already been opened, so simulate new connection. + onconnect() + } } joinGame(clientIP: string | null, numTurns: number) { @@ -158,11 +173,12 @@ export class Transport { gameID: this.gameID, }) this.sendMsg(JSON.stringify(msg)) + this.socket.close() } else { console.log('WebSocket is not open. Current state:', this.socket.readyState); console.log('attempting reconnect') } - this.socket = null + this.socket.onclose = (event: CloseEvent) => { } } private onSendAllianceRequest(event: SendAllianceRequestIntentEvent) { @@ -279,7 +295,15 @@ export class Transport { if (this.isLocal) { this.localServer.onMessage(msg) } else { - this.socket.send(msg) + if (this.socket.readyState == WebSocket.CLOSED || this.socket.readyState == WebSocket.CLOSED) { + console.warn('socket not ready, closing and trying later') + this.socket.close() + this.socket = null + this.connectRemote(this.onconnect, this.onmessage) + this.buffer.push(msg) + } else { + this.socket.send(msg) + } } } } \ No newline at end of file diff --git a/src/core/game/TerrainMapLoader.ts b/src/core/game/TerrainMapLoader.ts index 0472718aa..c7dc54d18 100644 --- a/src/core/game/TerrainMapLoader.ts +++ b/src/core/game/TerrainMapLoader.ts @@ -9,6 +9,8 @@ const maps = new Map() .set(GameMap.World, {bin: worldBin, info: worldInfo}) .set(GameMap.Europe, {bin: europeBin, info: europeInfo}); +const loadedMaps = new Map() + export interface NationMap { name: string; width: number; @@ -52,6 +54,9 @@ export class Terrain { } export async function loadTerrainMap(map: GameMap): Promise { + if (loadedMaps.has(map)) { + return loadedMaps.get(map) + } const mapData = maps.get(map) @@ -112,8 +117,9 @@ export async function loadTerrainMap(map: GameMap): Promise { terrain[x][y].land = land } } - - return new TerrainMap(terrain, numLand, mapData.info); + const m = new TerrainMap(terrain, numLand, mapData.info); + loadedMaps.set(map, m) + return m } function logBinaryAsAscii(data: string, length: number = 8) {