import { v4 as uuidv4 } from 'uuid'; import net from 'net'; import logger from './logger'; export interface Player { id: string; name: string; socket: net.Socket; joinedAt: Date; } export interface RoomInfo { id: string; name: string; hostId: string; hostName: string; hostPort: number; gameVersion: string; gameEdition: 'java' | 'bedrock'; maxPlayers: number; currentPlayers: number; nodeId: string; createdAt: Date; password?: string; } export class Room { public id: string; public name: string; public hostId: string; public hostName: string; public hostSocket: net.Socket | null = null; public hostPort: number; public gameVersion: string; public gameEdition: 'java' | 'bedrock'; public maxPlayers: number; public password?: string; public nodeId: string; public createdAt: Date; public lastActivity: Date; public bytesIn: number = 0; public bytesOut: number = 0; public players: Map = new Map(); private playerSockets: Map = new Map(); constructor(options: { name: string; hostName: string; hostPort: number; gameVersion: string; gameEdition: 'java' | 'bedrock'; maxPlayers: number; nodeId: string; password?: string; }) { this.id = uuidv4().substring(0, 8).toUpperCase(); this.name = options.name; this.hostId = uuidv4(); this.hostName = options.hostName; this.hostPort = options.hostPort; this.gameVersion = options.gameVersion; this.gameEdition = options.gameEdition; this.maxPlayers = options.maxPlayers; this.nodeId = options.nodeId; this.password = options.password; this.createdAt = new Date(); this.lastActivity = new Date(); } checkPassword(pwd?: string): boolean { if (!this.password) return true; return this.password === pwd; } touch(): void { this.lastActivity = new Date(); } addTraffic(bytesIn: number, bytesOut: number): void { this.bytesIn += bytesIn; this.bytesOut += bytesOut; this.touch(); } kickPlayer(playerId: string, reason?: string): boolean { const player = this.players.get(playerId); if (!player) return false; logger.info(`Player ${player.name} kicked from room ${this.name}: ${reason || 'no reason'}`); try { player.socket.destroy(); } catch (e) { /* ignore */ } this.players.delete(playerId); this.playerSockets.delete(playerId); return true; } isExpired(maxIdleMs: number): boolean { if (this.currentPlayers > 0) return false; return Date.now() - this.lastActivity.getTime() > maxIdleMs; } get currentPlayers(): number { return this.players.size; } get isFull(): boolean { return this.currentPlayers >= this.maxPlayers; } addPlayer(name: string, socket: net.Socket): Player { const player: Player = { id: uuidv4(), name, socket, joinedAt: new Date(), }; this.players.set(player.id, player); this.playerSockets.set(player.id, socket); logger.info(`Player ${name} joined room ${this.name} (${this.id})`); return player; } removePlayer(playerId: string): void { const player = this.players.get(playerId); if (player) { logger.info(`Player ${player.name} left room ${this.name} (${this.id})`); this.players.delete(playerId); this.playerSockets.delete(playerId); } } getPlayerList(): { id: string; name: string; joinedAt: Date }[] { return Array.from(this.players.values()).map(p => ({ id: p.id, name: p.name, joinedAt: p.joinedAt, })); } toInfo(): RoomInfo { return { id: this.id, name: this.name, hostId: this.hostId, hostName: this.hostName, hostPort: this.hostPort, gameVersion: this.gameVersion, gameEdition: this.gameEdition, maxPlayers: this.maxPlayers, currentPlayers: this.currentPlayers, nodeId: this.nodeId, createdAt: this.createdAt, password: this.password ? '***' : undefined, }; } destroy(): void { for (const [, player] of this.players) { try { player.socket.destroy(); } catch (e) { /* ignore */ } } if (this.hostSocket) { try { this.hostSocket.destroy(); } catch (e) { /* ignore */ } } this.players.clear(); this.playerSockets.clear(); logger.info(`Room ${this.name} (${this.id}) destroyed`); } } export class RoomManager { private rooms: Map = new Map(); private maxRooms: number; constructor(maxRooms: number) { this.maxRooms = maxRooms; } createRoom(options: { name: string; hostName: string; hostPort: number; gameVersion: string; gameEdition: 'java' | 'bedrock'; maxPlayers: number; nodeId: string; password?: string; }): Room | null { if (this.rooms.size >= this.maxRooms) { logger.warn('Max rooms reached, cannot create new room'); return null; } const room = new Room(options); this.rooms.set(room.id, room); logger.info(`Room created: ${room.name} (${room.id}) on node ${room.nodeId}`); return room; } getRoom(roomId: string): Room | undefined { return this.rooms.get(roomId); } deleteRoom(roomId: string): boolean { const room = this.rooms.get(roomId); if (room) { room.destroy(); this.rooms.delete(roomId); return true; } return false; } listRooms(): RoomInfo[] { return Array.from(this.rooms.values()).map(r => r.toInfo()); } getRoomCount(): number { return this.rooms.size; } getTotalPlayers(): number { let total = 0; for (const [, room] of this.rooms) { total += room.currentPlayers; } return total; } getTrafficStats(): { totalBytesIn: number; totalBytesOut: number } { let totalBytesIn = 0; let totalBytesOut = 0; for (const [, room] of this.rooms) { totalBytesIn += room.bytesIn; totalBytesOut += room.bytesOut; } return { totalBytesIn, totalBytesOut }; } cleanupExpiredRooms(maxIdleMs: number = 30 * 60 * 1000): number { let cleaned = 0; for (const [id, room] of this.rooms) { if (room.isExpired(maxIdleMs)) { logger.info(`Room ${room.name} (${id}) expired, cleaning up`); room.destroy(); this.rooms.delete(id); cleaned++; } } return cleaned; } startCleanupTimer(intervalMs: number = 5 * 60 * 1000, maxIdleMs: number = 30 * 60 * 1000): void { setInterval(() => { const cleaned = this.cleanupExpiredRooms(maxIdleMs); if (cleaned > 0) { logger.info(`Cleaned up ${cleaned} expired rooms`); } }, intervalMs); } }