changes
This commit is contained in:
39
src/server/controllers/GameController.ts
Normal file
39
src/server/controllers/GameController.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import { Request, Response } from "express";
|
||||
import { BaseController } from "./BaseController";
|
||||
import { SessionService } from "../services/SessionService";
|
||||
|
||||
export class GameController extends BaseController {
|
||||
private sessionService: SessionService = new SessionService();
|
||||
|
||||
public async createMatch(req: Request, res: Response) {
|
||||
try {
|
||||
const { user, body } = req;
|
||||
const { sessionName, seed } = body;
|
||||
const sessionId = await this.sessionService.createSession(user, sessionName, seed);
|
||||
res.status(201).json({ sessionId });
|
||||
} catch (error) {
|
||||
this.handleError(res, error);
|
||||
}
|
||||
}
|
||||
|
||||
public joinMatch(req: Request, res: Response) {
|
||||
try {
|
||||
const { user, body } = req;
|
||||
const { sessionId } = body;
|
||||
this.sessionService.joinSession(user, sessionId);
|
||||
res.status(200).json({ status: 'ok' });
|
||||
} catch (error) {
|
||||
this.handleError(res, error);
|
||||
}
|
||||
}
|
||||
|
||||
public listMatches(req: Request, res: Response) {
|
||||
try {
|
||||
this.sessionService.listSessions().then((sessions) => {
|
||||
res.status(200).json(sessions);
|
||||
});
|
||||
} catch (error) {
|
||||
this.handleError(res, error);
|
||||
}
|
||||
}
|
||||
}
|
@ -9,11 +9,11 @@ export abstract class BaseMongoManager {
|
||||
protected abstract collection?: string;
|
||||
logger = new LoggingService().logger;
|
||||
|
||||
create(data: Entity) {
|
||||
create(data: Entity): Promise<ObjectId | undefined>{
|
||||
return mongoExecute(
|
||||
async ({ collection }) => {
|
||||
await collection?.insertOne(data as any);
|
||||
return data;
|
||||
const result = await collection?.insertOne(data as any);
|
||||
return result?.insertedId;
|
||||
},
|
||||
{ colName: this.collection }
|
||||
);
|
||||
|
@ -1,86 +1,38 @@
|
||||
import { MatchSession } from "../../game/MatchSession";
|
||||
import { NetworkPlayer } from "../../game/entities/player/NetworkPlayer";
|
||||
import { SessionService } from "../services/SessionService";
|
||||
|
||||
import { ManagerBase } from "./ManagerBase";
|
||||
|
||||
export class SessionManager extends ManagerBase {
|
||||
private static sessions: any = {};
|
||||
private sessionService: SessionService = new SessionService();
|
||||
private static sessions: Map<string, MatchSession> = new Map();
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.logger.info('SessionController created');
|
||||
}
|
||||
|
||||
createSession(data: any, socketId: string): any {
|
||||
const { user, sessionName } = data;
|
||||
const player = new NetworkPlayer(user, socketId);
|
||||
const session = new MatchSession(player, sessionName);
|
||||
SessionManager.sessions[session.id] = session;
|
||||
this.sessionService.createSession(session);
|
||||
|
||||
return {
|
||||
status: 'ok',
|
||||
sessionId: session.id,
|
||||
playerId: player.id
|
||||
};
|
||||
}
|
||||
|
||||
joinSession(data: any, socketId: string): any {
|
||||
this.logger.debug('joinSession data :>> ')
|
||||
this.logger.object(data);
|
||||
const { user, sessionId } = data;
|
||||
const session: MatchSession = SessionManager.sessions[sessionId];
|
||||
const player = new NetworkPlayer(user, socketId);
|
||||
session.addPlayer(player);
|
||||
this.sessionService.updateSession(session);
|
||||
return {
|
||||
status: 'ok',
|
||||
sessionId: session.id,
|
||||
playerId: player.id
|
||||
};
|
||||
}
|
||||
|
||||
setPlayerReady(data: any): any {
|
||||
const { user, sessionId } = data;
|
||||
const session: MatchSession = SessionManager.sessions[sessionId];
|
||||
session.setPlayerReady(user)
|
||||
}
|
||||
|
||||
startSession(data: any): any {
|
||||
const sessionId: string = data.sessionId;
|
||||
const seed: string | undefined = data.seed;
|
||||
const session = SessionManager.sessions[sessionId];
|
||||
|
||||
if (!session) {
|
||||
return ({
|
||||
status: 'error',
|
||||
message: 'Session not found'
|
||||
});
|
||||
} else if (session.gameInProgress) {
|
||||
return {
|
||||
status: 'error',
|
||||
message: 'Game already in progress'
|
||||
};
|
||||
} else {
|
||||
const missingHumans = session.maxPlayers - session.numPlayers;
|
||||
for (let i = 0; i < missingHumans; i++) {
|
||||
session.addPlayer(session.createPlayerAI(i));
|
||||
}
|
||||
session.start(seed);
|
||||
return {
|
||||
status: 'ok'
|
||||
};
|
||||
getSessionPlayer(sessionId: string, userId: string): any {
|
||||
const session: MatchSession | undefined = SessionManager.sessions.get(sessionId);
|
||||
if (session !== undefined) {
|
||||
return session.players.find(player => player.id === userId);
|
||||
}
|
||||
}
|
||||
|
||||
updateSocketId(sessionId: string, userId: string, socketId: string): any {
|
||||
const player = this.getSessionPlayer(sessionId, userId);
|
||||
if (player !== undefined) {
|
||||
player.socketId = socketId;
|
||||
}
|
||||
}
|
||||
|
||||
setSession(session: MatchSession) {
|
||||
SessionManager.sessions.set(session.id, session);
|
||||
}
|
||||
|
||||
deleteSession(session: MatchSession) {
|
||||
SessionManager.sessions.delete(session.id);
|
||||
}
|
||||
|
||||
getSession(id: string) {
|
||||
return SessionManager.sessions[id];
|
||||
}
|
||||
|
||||
deleteSession(id: string) {
|
||||
delete SessionManager.sessions[id];
|
||||
}
|
||||
return SessionManager.sessions.get(id);
|
||||
}
|
||||
}
|
@ -3,6 +3,7 @@ import { AuthController } from '../controllers/AuthController';
|
||||
|
||||
import adminRouter from './adminRouter';
|
||||
import userRouter from './userRouter';
|
||||
import gameRouter from './gameRouter';
|
||||
|
||||
export default function(): Router {
|
||||
const router = Router();
|
||||
@ -15,8 +16,10 @@ export default function(): Router {
|
||||
router.post('/auth/code', (req: Request, res: Response) => authController.twoFactorCodeAuthentication(req, res));
|
||||
router.post('/login', (req: Request, res: Response) => authController.login(req, res));
|
||||
|
||||
router .use('/admin', adminRouter());
|
||||
router .use('/user', userRouter());
|
||||
router.use('/admin', adminRouter());
|
||||
router.use('/user', userRouter());
|
||||
router.use('/game', gameRouter());
|
||||
|
||||
|
||||
return router;
|
||||
}
|
||||
|
17
src/server/router/gameRouter.ts
Normal file
17
src/server/router/gameRouter.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { Request, Response, Router } from 'express';
|
||||
import { AuthController } from '../controllers/AuthController';
|
||||
import { GameController } from '../controllers/GameController';
|
||||
|
||||
|
||||
|
||||
export default function(): Router {
|
||||
const router = Router();
|
||||
const gameController = new GameController();
|
||||
const { authenticate } = AuthController
|
||||
|
||||
router.post('/match', authenticate({ roles: ['user']}), (req: Request, res: Response) => gameController.createMatch(req, res));
|
||||
router.patch('/match/join', authenticate({ roles: ['user']}), (req: Request, res: Response) => gameController.joinMatch(req, res));
|
||||
router.get('/match/list', authenticate({ roles: ['user']}), (req: Request, res: Response) => gameController.listMatches(req, res));
|
||||
|
||||
return router;
|
||||
}
|
35
src/server/services/PlayerNotificationService.ts
Normal file
35
src/server/services/PlayerNotificationService.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { DominoesGame } from "../../game/DominoesGame";
|
||||
import { MatchSession } from "../../game/MatchSession";
|
||||
import { GameState } from "../../game/dto/GameState";
|
||||
import { PlayerInterface } from "../../game/entities/player/PlayerInterface";
|
||||
|
||||
export class PlayerNotificationService {
|
||||
|
||||
async notifyGameState(game: DominoesGame) {
|
||||
const gameState: GameState = game.getGameState();
|
||||
const { players } = game;
|
||||
let promises: Promise<void>[] = players.map(player => player.sendEventWithAck('update-game-state', gameState));
|
||||
return await Promise.all(promises);
|
||||
}
|
||||
|
||||
async notifyPlayersState(players: PlayerInterface[]) {
|
||||
let promises: Promise<void>[] = players.map(player => player.sendEventWithAck('update-player-state', player.getState()));
|
||||
return await Promise.all(promises);
|
||||
}
|
||||
|
||||
|
||||
async notifyMatchState(session: MatchSession) {
|
||||
const { players } = session;
|
||||
let promises: Promise<void>[] = players.map(player => player.sendEventWithAck('update-match-session-state', session.getState()));
|
||||
return await Promise.all(promises);
|
||||
}
|
||||
|
||||
async sendEventToPlayers(event: string, players: PlayerInterface[], data: any = {}) {
|
||||
let promises: Promise<void>[] = players.map(player => player.sendEvent(event, data));
|
||||
return await Promise.all(promises);
|
||||
}
|
||||
|
||||
async sendEvent(event: string, player: PlayerInterface, data: any = {}) {
|
||||
player.sendEvent(event, data)
|
||||
}
|
||||
}
|
@ -1,19 +1,107 @@
|
||||
import { SessionCreationError } from "../../common/errors/SessionCreationError";
|
||||
import { SessionNotFoundError } from "../../common/errors/SessionNotFoundError";
|
||||
import { wait, whileNotUndefined } from "../../common/utilities";
|
||||
import { NetworkPlayer } from "../../game/entities/player/NetworkPlayer";
|
||||
import { MatchSession } from "../../game/MatchSession";
|
||||
import { PlayerNotificationService } from "./PlayerNotificationService";
|
||||
import { matchSessionAdapter } from "../db/DbAdapter";
|
||||
import { DbMatchSession } from "../db/interfaces";
|
||||
import { MatchSessionMongoManager } from "../db/mongo/MatchSessionMongoManager";
|
||||
import { SessionManager } from "../managers/SessionManager";
|
||||
import { ServiceBase } from "./ServiceBase";
|
||||
import { SocketIoService } from "./SocketIoService";
|
||||
|
||||
export class SessionService extends ServiceBase{
|
||||
private dbManager: MatchSessionMongoManager = new MatchSessionMongoManager();
|
||||
private sessionManager: SessionManager = new SessionManager();
|
||||
private notifyService = new PlayerNotificationService();
|
||||
constructor() {
|
||||
super()
|
||||
}
|
||||
|
||||
public createSession(session: MatchSession): any {
|
||||
this.dbManager.create(matchSessionAdapter(session));
|
||||
public async createSession(user: any, sessionName: string, seed: string ): Promise<string> {
|
||||
let socketClient;
|
||||
try {
|
||||
socketClient = await whileNotUndefined(() => SocketIoService.getClient(user._id));
|
||||
} catch (error) {
|
||||
throw new SessionCreationError();
|
||||
}
|
||||
const player = new NetworkPlayer(user._id, user.username, socketClient.socketId);
|
||||
const session = new MatchSession(player, sessionName, seed);
|
||||
const dbSessionId = await this.dbManager.create(matchSessionAdapter(session));
|
||||
if (dbSessionId === undefined) {
|
||||
throw new SessionCreationError();
|
||||
}
|
||||
session.id = dbSessionId.toString();
|
||||
socketClient.sessionId = session.id;
|
||||
this.sessionManager.setSession(session);
|
||||
this.notifyService.notifyMatchState(session);
|
||||
this.notifyService.notifyPlayersState(session.players);
|
||||
return session.id;
|
||||
}
|
||||
|
||||
public updateSession(session: MatchSession): any {
|
||||
public async joinSession(user: any, sessionId: string): Promise<void> {
|
||||
const session: MatchSession | undefined = this.sessionManager.getSession(sessionId);
|
||||
if (session === undefined) {
|
||||
throw new SessionNotFoundError();
|
||||
} let socketClient;
|
||||
try {
|
||||
socketClient = await whileNotUndefined(() => SocketIoService.getClient(user._id));
|
||||
} catch (error) {
|
||||
throw new SessionCreationError();
|
||||
}
|
||||
const player = new NetworkPlayer(user._id, user.name, socketClient.socketId);
|
||||
session.addPlayer(player);
|
||||
socketClient.sessionId = session.id;
|
||||
this.dbManager.replaceOne({id: session.id}, matchSessionAdapter(session));
|
||||
}
|
||||
|
||||
public listSessions(): Promise<DbMatchSession[]> {
|
||||
return this.dbManager.listByFilter({});
|
||||
}
|
||||
|
||||
public updateSocketId(sessionId: string, userId: string, socketId: string): any {
|
||||
this.sessionManager.updateSocketId(sessionId, userId, socketId);
|
||||
}
|
||||
|
||||
setPlayerReady(data: any): any {
|
||||
const { userId, sessionId } = data;
|
||||
const session: MatchSession | undefined = this.sessionManager.getSession(sessionId);
|
||||
if (session !== undefined) {
|
||||
session.setPlayerReady(userId)
|
||||
this.notifyService.notifyMatchState(session);
|
||||
this.notifyService.notifyPlayersState(session.players);
|
||||
}
|
||||
}
|
||||
|
||||
startSession(data: any): any {
|
||||
const sessionId: string = data.sessionId;
|
||||
const seed: string | undefined = data.seed;
|
||||
const session: MatchSession | undefined = this.sessionManager.getSession(sessionId);
|
||||
|
||||
if (session === undefined) {
|
||||
return ({
|
||||
status: 'error',
|
||||
message: 'Session not found'
|
||||
});
|
||||
} else if (session.matchInProgress) {
|
||||
return {
|
||||
status: 'error',
|
||||
message: 'Game already in progress'
|
||||
};
|
||||
} else {
|
||||
const missingHumans = session.maxPlayers - session.numPlayers;
|
||||
for (let i = 0; i < missingHumans; i++) {
|
||||
session.addPlayer(session.createPlayerAI(i));
|
||||
}
|
||||
session.start();
|
||||
return {
|
||||
status: 'ok'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// public updateSession(session: MatchSession): any {
|
||||
// this.dbManager.replaceOne({id: session.id}, matchSessionAdapter(session));
|
||||
// }
|
||||
}
|
@ -2,14 +2,39 @@ import { Server as HttpServer } from "http";
|
||||
import { ServiceBase } from "./ServiceBase";
|
||||
import { Server } from "socket.io";
|
||||
import { SessionManager } from "../managers/SessionManager";
|
||||
import { SecurityManager } from "../managers/SecurityManager";
|
||||
import { User } from "../db/interfaces";
|
||||
import { Socket } from "socket.io";
|
||||
import { SessionService } from "./SessionService";
|
||||
|
||||
export class SocketIoService extends ServiceBase{
|
||||
io: Server
|
||||
clients: Map<string, any> = new Map();
|
||||
private static clients: Map<string, any> = new Map();
|
||||
private sessionService: SessionService = new SessionService();
|
||||
|
||||
static getClient(id: string) {
|
||||
return this.clients.get(id);
|
||||
}
|
||||
|
||||
security = new SecurityManager();
|
||||
constructor(private httpServer: HttpServer) {
|
||||
super()
|
||||
this.io = this.socketIo(httpServer);
|
||||
this.io = this.socketIo(httpServer);
|
||||
this.io.use(async (socket: Socket, next: any) => {
|
||||
if (socket.handshake.auth && socket.handshake.auth.token) {
|
||||
const token = socket.handshake.auth.token;
|
||||
try {
|
||||
const user: User = await this.security.verifyJwt(token);
|
||||
socket.user = user;
|
||||
next();
|
||||
} catch (err) {
|
||||
this.logger.error(err);
|
||||
next(new Error('Authentication error'));
|
||||
}
|
||||
} else {
|
||||
next(new Error('Authentication error'));
|
||||
}
|
||||
});
|
||||
this.initListeners();
|
||||
}
|
||||
|
||||
@ -17,8 +42,12 @@ export class SocketIoService extends ServiceBase{
|
||||
return this.io;
|
||||
}
|
||||
|
||||
getUserId(socket: Socket) {
|
||||
const { user } = socket;
|
||||
return user?._id?.toString();
|
||||
}
|
||||
|
||||
private initListeners() {
|
||||
const sessionController = new SessionManager();
|
||||
this.io.on('connection', (socket) => {
|
||||
this.logger.debug(`connect ${socket.id}`);
|
||||
if (socket.recovered) {
|
||||
@ -28,59 +57,79 @@ export class SocketIoService extends ServiceBase{
|
||||
this.logger.debug("socket.data:", socket.data);
|
||||
} else {
|
||||
this.logger.debug("new connection");
|
||||
this.clients.set(socket.id, { alive: true });
|
||||
socket.join('room-general')
|
||||
socket.data.foo = "bar";
|
||||
}
|
||||
|
||||
socket.on('pong', () => {
|
||||
if (this.clients.has(socket.id)) {
|
||||
this.clients.set(socket.id, { alive: true });
|
||||
const { id: socketId, user } = socket;
|
||||
if (user !== undefined && user._id !== undefined) {
|
||||
const userId = user._id.toString();
|
||||
if (!SocketIoService.clients.has(userId)) {
|
||||
SocketIoService.clients.set(userId, { socketId, alive: true, user: socket.user });
|
||||
socket.join('room-general')
|
||||
} else {
|
||||
const client = SocketIoService.clients.get(userId);
|
||||
this.sessionService.updateSocketId(client.sessionId, userId, socketId);
|
||||
client.socketId = socketId;
|
||||
this.logger.debug(`User '${user.username}' already connected. Updating socketId to ${socketId}`);
|
||||
client.alive = true;
|
||||
}
|
||||
} else {
|
||||
this.logger.error('User not found');
|
||||
socket.disconnect();
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
socket.on('disconnect', () => {
|
||||
this.logger.debug('user disconnected');
|
||||
this.clients.delete(socket.id);
|
||||
const id = this.getUserId(socket);
|
||||
if (id) {
|
||||
this.logger.info('user disconnected');
|
||||
SocketIoService.clients.delete(id);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('createSession', (data, callback) => {
|
||||
const response = sessionController.createSession(data, socket.id);
|
||||
callback(response);
|
||||
});
|
||||
// socket.on('createSession', (data, callback) => {
|
||||
// const response = sessionController.createSession(data, socket.id);
|
||||
// callback(response);
|
||||
// });
|
||||
|
||||
socket.on('startSession', (data, callback) => {
|
||||
const response = sessionController.startSession(data);
|
||||
const response = this.sessionService.startSession(data);
|
||||
callback(response);
|
||||
});
|
||||
|
||||
socket.on('joinSession', (data, callback) => {
|
||||
const response = sessionController.joinSession(data, socket.id);
|
||||
callback(response);
|
||||
});
|
||||
// socket.on('joinSession', (data, callback) => {
|
||||
// const response = sessionController.joinSession(data, socket.id);
|
||||
// callback(response);
|
||||
// });
|
||||
|
||||
socket.on('playerReady', (data, callback) => {
|
||||
const response = sessionController.setPlayerReady(data);
|
||||
const response = this.sessionService.setPlayerReady(data);
|
||||
callback(response);
|
||||
});
|
||||
|
||||
socket.on('pong', () => {
|
||||
const id = this.getUserId(socket);
|
||||
if (id && SocketIoService.clients.has(id)) {
|
||||
const client = SocketIoService.clients.get(id);
|
||||
SocketIoService.clients.set(id, {...client, alive: true });
|
||||
}
|
||||
})
|
||||
|
||||
this.pingClients()
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
|
||||
private pingClients() {
|
||||
setInterval(() => {
|
||||
for (let [id, client] of this.clients.entries()) {
|
||||
for (let [id, client] of SocketIoService.clients.entries()) {
|
||||
if (!client.alive) {
|
||||
this.logger.debug(`Client ${id} did not respond. Disconnecting.`);
|
||||
this.io.to(id).disconnectSockets(true); // Disconnect client
|
||||
this.clients.delete(id);
|
||||
SocketIoService.clients.delete(id);
|
||||
} else {
|
||||
client.alive = false; // Reset alive status for the next ping
|
||||
this.io.to(id).emit('ping'); // Send ping message
|
||||
this.io.to(client.socketId).emit('ping'); // Send ping message
|
||||
}
|
||||
}
|
||||
}, 30000);
|
||||
}, 10000);
|
||||
}
|
||||
|
||||
private socketIo(httpServer: HttpServer): Server {
|
||||
|
8
src/server/types/socket/index.d.ts
vendored
Normal file
8
src/server/types/socket/index.d.ts
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
import { Socket } from 'socket.io';
|
||||
import { User } from '../../db/interfaces';
|
||||
|
||||
declare module 'socket.io' {
|
||||
interface Socket {
|
||||
user?: User;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user