rewrite game join network logic: don't load client game until game starts. Allows setting attrs like game map, difficulty

This commit is contained in:
evanpelle
2024-10-16 19:52:05 -07:00
parent 9b789b7006
commit d92dc9eba6
5 changed files with 131 additions and 81 deletions
+70 -45
View File
@@ -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<ClientGame> {
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<GameRunner> {
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<ClientGa
)
}
export class ClientGame {
export class GameRunner {
private myPlayer: Player
private turns: Turn[] = []
private isActive = false
@@ -73,33 +105,8 @@ export class ClientGame {
private transport: Transport,
) { }
public join(onstart: () => 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
+1 -1
View File
@@ -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',
+12 -17
View File
@@ -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<string | null> = 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) {
+40 -16
View File
@@ -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)
}
}
}
}
+8 -2
View File
@@ -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<GameMap, TerrainMap>()
export interface NationMap {
name: string;
width: number;
@@ -52,6 +54,9 @@ export class Terrain {
}
export async function loadTerrainMap(map: GameMap): Promise<TerrainMap> {
if (loadedMaps.has(map)) {
return loadedMaps.get(map)
}
const mapData = maps.get(map)
@@ -112,8 +117,9 @@ export async function loadTerrainMap(map: GameMap): Promise<TerrainMap> {
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) {