mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 09:30:45 +00:00
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:
+70
-45
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user