diff --git a/src/Client.ts b/src/Client.ts index 3936312..d3ad349 100644 --- a/src/Client.ts +++ b/src/Client.ts @@ -101,6 +101,14 @@ class Client { setSkips = (skips: number) => { this.skips = skips } + + /** Adopt the connection from another Client (used on reconnect) */ + replaceConnection = (other: Client) => { + this.address = other.address + this.sendAction = other.sendAction + this.closeConnection = other.closeConnection + this.reconnectToken = other.reconnectToken + } } export default Client diff --git a/src/Lobby.ts b/src/Lobby.ts index eaea703..388f7db 100644 --- a/src/Lobby.ts +++ b/src/Lobby.ts @@ -32,7 +32,7 @@ export const getEnemy = (client: Client): [Lobby | null, Client | null] => { const RECONNECT_GRACE_PERIOD = 60000; interface DisconnectedSlot { - reconnectToken: string; + client: Client; role: 'host' | 'guest'; timer: ReturnType; } @@ -130,7 +130,7 @@ class Lobby { console.log(`Player ${client.id} disconnected from lobby ${this.code}, reserving slot for ${RECONNECT_GRACE_PERIOD / 1000}s`) this.disconnectedSlot = { - reconnectToken: client.reconnectToken, + client, role, timer: setTimeout(() => { // Grace period expired, do a full leave @@ -152,40 +152,55 @@ class Lobby { enemy?.sendAction({ action: "enemyDisconnected" }); }; - /** Reconnecting client reclaims their slot */ - rejoin = (newClient: Client, reconnectToken: string): boolean => { - if (!this.disconnectedSlot || this.disconnectedSlot.reconnectToken !== reconnectToken) { - return false; + /** Reconnecting client reclaims their slot. + * Returns the restored Client (with all game state intact) on success, or null on failure. + * The caller MUST use the returned client for all future messages on this socket. */ + rejoin = (newClient: Client, reconnectToken: string): Client | null => { + if (!this.disconnectedSlot || this.disconnectedSlot.client.reconnectToken !== reconnectToken) { + return null; } - const { role, timer } = this.disconnectedSlot; + const { client: oldClient, role, timer } = this.disconnectedSlot; clearTimeout(timer); this.disconnectedSlot = null; - // Place the new client in the correct slot + // Swap the new socket/connection onto the old client, preserving all game state + oldClient.replaceConnection(newClient); + + // Place the old client back in the correct slot if (role === 'host') { - this.host = newClient; + this.host = oldClient; } else { - this.guest = newClient; + this.guest = oldClient; } - newClient.setLobby(this); - this.handyAllowMPExtension.set(newClient.id, false); + oldClient.setLobby(this); + this.handyAllowMPExtension.set(oldClient.id, false); // Send rejoin confirmation with new reconnect token - newClient.sendAction({ + oldClient.sendAction({ action: "rejoinedLobby", code: this.code, type: this.gameMode, - reconnectToken: newClient.reconnectToken, + reconnectToken: oldClient.reconnectToken, }); // Notify the other player const enemy = role === 'host' ? this.guest : this.host; enemy?.sendAction({ action: "enemyReconnected" }); + // Re-sync game state so both sides have correct values + oldClient.sendAction({ action: "playerInfo", lives: oldClient.lives }); + enemy?.sendAction({ + action: "enemyInfo", + handsLeft: oldClient.handsLeft, + score: oldClient.score.toString(), + skips: oldClient.skips, + lives: oldClient.lives, + }); + this.broadcastLobbyInfo(); - return true; + return oldClient; }; join = (client: Client) => { diff --git a/src/actionHandlers.ts b/src/actionHandlers.ts index f7b1330..642f6b4 100644 --- a/src/actionHandlers.ts +++ b/src/actionHandlers.ts @@ -84,7 +84,7 @@ const disconnectFromLobbyAction = (client: Client) => { const rejoinLobbyAction = ( { code, reconnectToken }: ActionHandlerArgs, client: Client, -) => { +): Client | undefined => { const lobby = Lobby.get(code); if (!lobby) { client.sendAction({ @@ -94,12 +94,15 @@ const rejoinLobbyAction = ( return; } - if (!lobby.rejoin(client, reconnectToken)) { + const restoredClient = lobby.rejoin(client, reconnectToken); + if (!restoredClient) { client.sendAction({ action: "error", message: "Could not rejoin lobby. Token invalid or slot expired.", }); + return; } + return restoredClient; }; const lobbyInfoAction = (client: Client) => { diff --git a/src/main.ts b/src/main.ts index 7af973d..d3d12fa 100644 --- a/src/main.ts +++ b/src/main.ts @@ -122,7 +122,7 @@ const server = createServer((socket) => { // Enable OS-level TCP keepalive as secondary dead connection detection socket.setKeepAlive(true, 10000) - const client = new Client(socket.address(), sendActionToSocket(socket), socket.end) + let client = new Client(socket.address(), sendActionToSocket(socket), socket.end) client.sendAction({ action: 'connected' }) client.sendAction({ action: 'version' }) @@ -205,12 +205,14 @@ const server = createServer((socket) => { client, ) break - case 'rejoinLobby': - actionHandlers.rejoinLobby( + case 'rejoinLobby': { + const restored = actionHandlers.rejoinLobby( actionArgs as ActionHandlerArgs, client, ) + if (restored) client = restored break + } case 'lobbyInfo': actionHandlers.lobbyInfo(client) break