diff --git a/resources/lang/en.json b/resources/lang/en.json index 92111a61b..2946e5e2f 100644 --- a/resources/lang/en.json +++ b/resources/lang/en.json @@ -453,6 +453,7 @@ }, "error_modal": { "crashed": "Game crashed!", + "connection_error": "Connection error!", "paste_discord": "Please paste the following in your bug report in Discord:", "copy_clipboard": "Copy to clipboard", "copied": "Copied!", diff --git a/src/client/ClientGameRunner.ts b/src/client/ClientGameRunner.ts index 401ea2653..80771b257 100644 --- a/src/client/ClientGameRunner.ts +++ b/src/client/ClientGameRunner.ts @@ -99,6 +99,17 @@ export function joinLobby( terrainLoad, ).then((r) => r.start()); } + if (message.type === "error") { + showErrorModal( + message.error, + "", + lobbyConfig.gameID, + lobbyConfig.clientID, + true, + false, + "error_modal.connection_error", + ); + } }; transport.connect(onconnect, onmessage); return () => { @@ -309,7 +320,19 @@ export class ClientGameRunner { this.lobby.gameStartInfo.gameID, this.lobby.clientID, true, - translateText("error_modal.desync_notice"), + false, + "error_modal.desync_notice", + ); + } + if (message.type === "error") { + showErrorModal( + message.error, + "", + this.lobby.gameID, + this.lobby.clientID, + true, + false, + "error_modal.connection_error", ); } if (message.type === "turn") { @@ -538,7 +561,8 @@ function showErrorModal( gameID: GameID, clientID: ClientID, closable = false, - heading = translateText("error_modal.crashed"), + showDiscord = true, + heading = "error_modal.crashed", ) { const errorText = `Error: ${errMsg}\nStack: ${stack}`; @@ -550,7 +574,9 @@ function showErrorModal( modal.id = "error-modal"; - const content = `${translateText(heading)}\n game id: ${gameID}, client id: ${clientID}\n${translateText("error_modal.paste_discord")}\n${errorText}`; + const discord = showDiscord ? translateText("error_modal.paste_discord") : ""; + + const content = `${discord}\n${translateText(heading)}\n game id: ${gameID}, client id: ${clientID}\n${errorText}`; // Create elements const pre = document.createElement("pre"); diff --git a/src/client/Transport.ts b/src/client/Transport.ts index 7d112c8ad..f4b732f8d 100644 --- a/src/client/Transport.ts +++ b/src/client/Transport.ts @@ -327,8 +327,8 @@ export class Transport { console.log( `WebSocket closed. Code: ${event.code}, Reason: ${event.reason}`, ); - if (event.code !== 1000) { - console.log(`reconnecting`); + if (event.code !== 1000 && event.code !== 1002) { + console.log(`recieved error code ${event.code}, reconnecting`); this.reconnect(); } }; diff --git a/src/core/Schemas.ts b/src/core/Schemas.ts index 471dd647f..bb0ca9550 100644 --- a/src/core/Schemas.ts +++ b/src/core/Schemas.ts @@ -80,7 +80,8 @@ export type ServerMessage = | ServerStartGameMessage | ServerPingMessage | ServerDesyncMessage - | ServerPrestartMessage; + | ServerPrestartMessage + | ServerErrorMessage; export type ServerSyncMessage = z.infer; export type ServerStartGameMessage = z.infer< @@ -89,6 +90,7 @@ export type ServerStartGameMessage = z.infer< export type ServerPingMessage = z.infer; export type ServerDesyncMessage = z.infer; export type ServerPrestartMessage = z.infer; +export type ServerErrorMessage = z.infer; export type ClientSendWinnerMessage = z.infer; export type ClientPingMessage = z.infer; export type ClientIntentMessage = z.infer; @@ -330,7 +332,7 @@ export const TurnSchema = z.object({ // Server const ServerBaseMessageSchema = z.object({ - type: z.enum(["turn", "ping", "prestart", "start", "desync"]), + type: z.enum(["turn", "ping", "prestart", "start", "desync", "error"]), }); export const ServerTurnMessageSchema = ServerBaseMessageSchema.extend({ @@ -375,12 +377,18 @@ export const ServerDesyncSchema = ServerBaseMessageSchema.extend({ yourHash: z.number().optional(), }); +export const ServerErrorSchema = ServerBaseMessageSchema.extend({ + type: z.literal("error"), + error: z.string(), +}); + export const ServerMessageSchema = z.discriminatedUnion("type", [ ServerTurnMessageSchema, ServerPrestartMessageSchema, ServerStartGameMessageSchema, ServerPingMessageSchema, ServerDesyncSchema, + ServerErrorSchema, ]); // Client diff --git a/src/server/GameServer.ts b/src/server/GameServer.ts index 3b538cb32..37c51c7f1 100644 --- a/src/server/GameServer.ts +++ b/src/server/GameServer.ts @@ -13,6 +13,7 @@ import { Intent, PlayerRecord, ServerDesyncSchema, + ServerErrorMessage, ServerPrestartMessageSchema, ServerStartGameMessageSchema, ServerTurnMessageSchema, @@ -195,7 +196,16 @@ export class GameServer { this.log.error("Failed to parse client message", error, { clientID: client.clientID, }); - client.ws.close(1002, "ClientMessageSchema"); + client.ws.send( + JSON.stringify({ + type: "error", + error: error.toString(), + } satisfies ServerErrorMessage), + ); + // Add a small delay before closing the connection to ensure the error message is received + setTimeout(() => { + client.ws.close(1002, "ClientMessageSchema"); + }, 100); return; } const clientMsg = parsed.data; @@ -543,11 +553,20 @@ export class GameServer { clientID: client.clientID, persistentID: client.persistentID, }); - client.ws.close(1000, "Kicked from game"); - this.activeClients = this.activeClients.filter( - (c) => c.clientID !== clientID, + client.ws.send( + JSON.stringify({ + type: "error", + error: "Kicked from game (you may have been playing on another tab)", + } satisfies ServerErrorMessage), ); - this.kickedClients.add(clientID); + // Add a small delay before closing the connection to ensure the error message is received + setTimeout(() => { + client.ws.close(1000, "Kicked from game"); + this.activeClients = this.activeClients.filter( + (c) => c.clientID !== clientID, + ); + this.kickedClients.add(clientID); + }, 100); } else { this.log.warn(`cannot kick client, not found in game`, { clientID, diff --git a/src/server/Worker.ts b/src/server/Worker.ts index 57db1c8b2..427e369b0 100644 --- a/src/server/Worker.ts +++ b/src/server/Worker.ts @@ -13,6 +13,7 @@ import { ClientJoinMessageSchema, GameRecord, GameRecordSchema, + ServerErrorMessage, } from "../core/Schemas"; import { CreateGameInputSchema, GameInputSchema } from "../core/WorkerSchemas"; import { archive, readGameRecord } from "./Archive"; @@ -300,6 +301,12 @@ export function startWorker() { if (!parsed.success) { const error = z.prettifyError(parsed.error); log.warn("Error parsing join message client", error); + ws.send( + JSON.stringify({ + type: "error", + error: error.toString(), + } satisfies ServerErrorMessage), + ); ws.close(1002, "ClientJoinMessageSchema"); return; }