Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Che bato/manual disconnect #425

Merged
merged 5 commits into from
Jan 10, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 66 additions & 46 deletions server/gamenode/GameServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import http from 'http';
import https from 'https';
import express from 'express';
import cors from 'cors';
import socketio from 'socket.io';
import type { Socket as IOSocket, DefaultEventsMap } from 'socket.io';
import { Server as IOServer } from 'socket.io';
import { v4 as uuid } from 'uuid';

import { logger } from '../logger';
Expand All @@ -22,12 +23,20 @@ interface User {
username: string;
}

/**
* Represents additional Socket types we can leverage these later.
*/

interface SocketData {
manualDisconnect?: boolean;
}

/**
* Represents a player waiting in the queue.
*/
interface QueuedPlayer {
deck: Deck;
socket?: socketio.Socket;
socket?: Socket;
user: User;
}

Expand All @@ -37,7 +46,7 @@ export class GameServer {
private protocol = 'https';
private host = env.gameNodeHost;
private queue: QueuedPlayer[] = [];
private io: socketio.Server;
private io: IOServer;
private titleCardData: any;
private shortCardData: any;

Expand Down Expand Up @@ -75,15 +84,21 @@ export class GameServer {
? 'https://tbd.com'
: 'http://localhost:3000';

this.io = new socketio.Server(server, {
this.io = new IOServer(server, {
perMessageDeflate: false,
cors: {
origin: corsOrigin,
methods: ['GET', 'POST']
}
});

this.io.on('connection', (socket) => this.onConnection(socket));
// Currently for IOSockets we can use DefaultEventsMap but later we can customize these.
this.io.on('connection', (socket: IOSocket<DefaultEventsMap, DefaultEventsMap, DefaultEventsMap, SocketData>) => {
this.onConnection(socket);
socket.on('manualDisconnect', () => {
socket.data.manualDisconnect = true;
socket.disconnect();
});
});
}

private setupAppRoutes(app: express.Application) {
Expand Down Expand Up @@ -124,7 +139,7 @@ export class GameServer {
});
app.post('/api/enter-queue', (req, res) => {
const { user, deck } = req.body;
const success = this.enterQueue(user, deck, null);
const success = this.enterQueue(user, deck);
if (!success) {
return res.status(400).json({ success: false, message: 'Failed to enter queue' });
}
Expand Down Expand Up @@ -275,7 +290,7 @@ export class GameServer {
lobby.addLobbyUser(user, socket);

socket.send('connectedUser', user.id);
socket.on('disconnect', (_, reason) => this.onSocketDisconnected(user.id, reason));
socket.on('disconnect', () => this.onSocketDisconnected(ioSocket, user.id));
return;
}

Expand All @@ -301,37 +316,36 @@ export class GameServer {
lobby.addLobbyUser(newUser, socket);
this.userLobbyMap.set(newUser.id, lobby.id);
socket.send('connectedUser', newUser.id);
socket.on('disconnect', (_, reason) => this.onSocketDisconnected(user.id, reason));
socket.on('disconnect', () => this.onSocketDisconnected(ioSocket, user.id));
return;
}

lobby.addLobbyUser(user, socket);
this.userLobbyMap.set(user.id, lobby.id);
return;
}
// if they are not in the lobby they could be in a queue
// 3. if they are not in the lobby they could be in a queue
const queuedPlayer = this.queue.find((p) => p.user.id === user.id);
if (queuedPlayer) {
queuedPlayer.socket = ioSocket;
queuedPlayer.socket = new Socket(ioSocket);

// handle queue-specific events and add lobby disconnect
ioSocket.on('disconnect', (reason) => {
this.onSocketDisconnected(user.id, reason);
});
ioSocket.on('disconnect', () => this.onSocketDisconnected(ioSocket, user.id));

this.matchmakeQueuePlayers();
return;
}

// A user should not get here
ioSocket.disconnect();
throw new Error(`Error state when connecting to lobby/game ${ioSocket.request.user.username} disconnecting`);
// this can happen when someone tries to reconnect to the game but are out of the mapping TODO make a notification for the player
logger.info(`Error state when connecting to lobby/game ${user.id} disconnecting`);
}

/**
* Put a user into the queue array.
* Put a user into the queue array. They always start with a null socket.
*/
private enterQueue(user: any, deck: any, socket: socketio.Socket | null): boolean {
private enterQueue(user: any, deck: any): boolean {
// Quick check: if they're already in a lobby, no queue
if (this.userLobbyMap.has(user.id)) {
logger.info(`User ${user.id} already in a lobby, ignoring queue request.`);
Expand All @@ -342,11 +356,10 @@ export class GameServer {
logger.info(`User ${user.id} is already in queue, rejoining`);
this.removeFromQueue(user.id);
}

this.queue.push({
user,
deck,
socket
socket: null
});
return true;
}
Expand All @@ -372,15 +385,15 @@ export class GameServer {
lobby.createLobbyUser(p2.user, p2.deck);

// Attach their sockets to the lobby (if they exist)
const socket1 = p1.socket ? new Socket(p1.socket) : null;
const socket2 = p2.socket ? new Socket(p2.socket) : null;
const socket1 = p1.socket ? p1.socket : null;
const socket2 = p2.socket ? p2.socket : null;
if (socket1) {
lobby.addLobbyUser(p1.user, socket1);
socket1.on('disconnect', (_, reason) => this.onSocketDisconnected(p1.user.id, reason));
socket1.on('disconnect', () => this.onSocketDisconnected(socket1.socket, p1.user.id));
}
if (socket2) {
lobby.addLobbyUser(p2.user, socket2);
socket2.on('disconnect', (_, reason) => this.onSocketDisconnected(p2.user.id, reason));
socket2.on('disconnect', () => this.onSocketDisconnected(socket2.socket, p2.user.id));
}

// Save user => lobby mapping
Expand All @@ -391,6 +404,8 @@ export class GameServer {
lobby.setLobbyOwner(p1.user.id);
lobby.setTokens();
lobby.setPlayableCardTitles();
// this needs to be here since we only send start game via the LobbyOwner.
lobby.setLobbyOwner(p1.user.id);
lobby.sendLobbyState();
logger.info(`Matched players ${p1.user.username} and ${p2.user.username} in lobby ${lobby.id}.`);
}
Expand All @@ -403,38 +418,43 @@ export class GameServer {
this.queue = this.queue.filter((q) => q.user.id !== userId);
}

public onSocketDisconnected(id: string, reason: string) {
public onSocketDisconnected(socket: IOSocket<DefaultEventsMap, DefaultEventsMap, DefaultEventsMap, SocketData>, id: string) {
if (!this.userLobbyMap.has(id)) {
this.removeFromQueue(id);
return;
}
const lobbyId = this.userLobbyMap.get(id);
const lobby = this.lobbies.get(lobbyId);
if (reason === 'client namespace disconnect') {


const wasManualDisconnect = !!socket?.data?.manualDisconnect;
if (wasManualDisconnect) {
this.userLobbyMap.delete(id);
lobby.removeUser(id);
} else if (reason === 'ping timeout' || reason === 'transport close') {
lobby.setUserDisconnected(id);
setTimeout(() => {
// Check if the user is still disconnected after the timer
if (lobby.getUserState(id) === 'disconnected') {
this.userLobbyMap.delete(id);
lobby.removeUser(id);
// Check if lobby is empty
if (lobby.isEmpty()) {
// Start the cleanup process
lobby.cleanLobby();
this.lobbies.delete(lobbyId);
}
}
}, 30000);
}

// check if lobby is empty
if (lobby.isEmpty()) {
// cleanup process
lobby.cleanLobby();
this.lobbies.delete(lobbyId);
// check if lobby is empty
if (lobby.isEmpty()) {
// cleanup process
lobby.cleanLobby();
this.lobbies.delete(lobbyId);
}
return;
}
// TODO perhaps add a timeout for lobbies so they clean themselves up if somehow they become empty
// without triggering onSocketDisconnect
lobby.setUserDisconnected(id);
setTimeout(() => {
CheBato marked this conversation as resolved.
Show resolved Hide resolved
// Check if the user is still disconnected after the timer
if (lobby.getUserState(id) === 'disconnected') {
this.userLobbyMap.delete(id);
lobby.removeUser(id);
// Check if lobby is empty
if (lobby.isEmpty()) {
// Start the cleanup process
lobby.cleanLobby();
this.lobbies.delete(lobbyId);
}
}
}, 20000);
}
}
Loading