This commit is contained in:
Jose Conde
2024-07-12 16:27:52 +02:00
parent ca0f1466c2
commit 5f117667a4
29 changed files with 823 additions and 407 deletions

View File

@ -124,14 +124,14 @@ export class AuthController extends BaseController {
return false;
}
const tokenFromDb = await new ApiTokenMongoManager().getById(token._id.toString());
const tokenFromDb: Token = await new ApiTokenMongoManager().getById(token._id.toString()) as Token;
if (!tokenFromDb) {
return false;
}
const { roles } = tokenFromDb;
const validRoles = rolesToCheck.filter((r: string) => roles.includes(r));
const validRoles = rolesToCheck.filter((r: string) => roles?.includes(r) || false);
return validRoles.length === rolesToCheck.length;
}

View File

@ -16,22 +16,42 @@ export class GameController extends BaseController {
}
}
public joinMatch(req: Request, res: Response) {
public async joinMatch(req: Request, res: Response) {
try {
const { user, body } = req;
const { sessionId } = body;
this.sessionService.joinSession(user, sessionId);
const { user, params } = req;
const { sessionId } = params;
await this.sessionService.joinSession(user, sessionId);
res.status(200).json({ status: 'ok' });
} catch (error) {
this.handleError(res, error);
}
}
public listMatches(req: Request, res: Response) {
public async listMatches(req: Request, res: Response) {
try {
this.sessionService.listSessions().then((sessions) => {
res.status(200).json(sessions);
});
const sessions = await this.sessionService.listJoinableSessions()
res.status(200).json(sessions);
} catch (error) {
this.handleError(res, error);
}
}
public async getMatch(req: Request, res: Response) {
try {
const { sessionId } = req.params;
const session = this.sessionService.getSession(sessionId)
res.status(200).json(session);
} catch (error) {
this.handleError(res, error);
}
}
public async deleteMatch(req: Request, res: Response) {
try {
const { sessionId } = req.params;
await this.sessionService.deleteSession(sessionId);
this.logger.info(`Session ${sessionId} deleted`);
res.status(200).json({ status: 'ok' });
} catch (error) {
this.handleError(res, error);
}

View File

@ -39,6 +39,10 @@ export interface User extends EntityMongo {
namespace?: Namespace;
}
export interface DbMatchSessionUpdate extends EntityMongo {
state?: string;
}
export interface DbMatchSession extends EntityMongo {
id: string;
name: string;

View File

@ -1,6 +1,6 @@
import { ObjectId } from "mongodb";
import { mongoExecute } from "./mongoDBPool";
import { Entity } from "../../interfaces";
import { Entity, EntityMongo } from "../../interfaces";
import { LoggingService } from "../../../../common/LoggingService";
import toObjectId from "./mongoUtils";
@ -9,7 +9,8 @@ export abstract class BaseMongoManager {
protected abstract collection?: string;
logger = new LoggingService().logger;
create(data: Entity): Promise<ObjectId | undefined>{
async create(data: Entity): Promise<ObjectId | undefined> {
this.stampEntity(data);
return mongoExecute(
async ({ collection }) => {
const result = await collection?.insertOne(data as any);
@ -19,25 +20,27 @@ export abstract class BaseMongoManager {
);
}
delete(id: string) {
async delete(id: string): Promise<number> {
return mongoExecute(
async ({ collection }) => {
await collection?.deleteOne({ _id: this.toObjectId(id) });
const result = await collection?.deleteOne({ _id: this.toObjectId(id) });
return result?.deletedCount || 0;
},
{ colName: this.collection }
);
}
deleteByFilter(filter: any) {
async deleteByFilter(filter: any): Promise<number> {
return mongoExecute(
async ({ collection }) => {
await collection?.deleteOne(filter);
const result = await collection?.deleteOne(filter);
return result?.deletedCount || 0;
},
{ colName: this.collection }
);
}
getById(id: string) {
async getById(id: string): Promise<EntityMongo | null> {
return mongoExecute(
async ({ collection }) => {
return await collection?.findOne({ _id: this.toObjectId(id) });
@ -46,7 +49,7 @@ export abstract class BaseMongoManager {
);
}
getByFilter(filter: any) {
async getByFilter(filter: any): Promise<EntityMongo | null> {
return mongoExecute(
async ({ collection }) => {
return await collection?.findOne(filter);
@ -55,51 +58,71 @@ export abstract class BaseMongoManager {
);
}
list() {
async list(sortCriteria?: any, pagination?: {pageSize: number, page: number}): Promise<EntityMongo[]> {
return mongoExecute(
async ({ collection }) => {
return await collection?.find().toArray();
const cursor = collection?.find();
if (sortCriteria) {
cursor?.sort(sortCriteria);
}
if (pagination) {
cursor?.skip(pagination.pageSize * (pagination.page - 1)).limit(pagination.pageSize);
}
return await cursor?.toArray();
},
{ colName: this.collection }
);
}
listByFilter(filter: any) {
async listByFilter(filter: any, sortCriteria?: any, pagination?: {pageSize: number, page: number}): Promise<EntityMongo[]> {
return mongoExecute(
async ({ collection }) => {
return await collection?.find(filter).toArray();
const cursor = collection?.find(filter);
if (sortCriteria) {
cursor?.sort(sortCriteria);
}
if (pagination) {
cursor?.skip(pagination.pageSize * (pagination.page - 1)).limit(pagination.pageSize);
}
return await cursor?.toArray();
},
{ colName: this.collection }
);
}
update(object: Entity) {
async update(object: EntityMongo): Promise<number> {
const data: any = { ...object };
const id = data._id;
delete data._id;
this.stampEntity(data, false);
delete data._id;
return mongoExecute(async ({ collection }) => {
return await collection?.updateOne(
const result = await collection?.updateOne(
{ _id: this.toObjectId(id) },
{ $set: data }
);
return result?.modifiedCount || 0;
},
{ colName: this.collection });
}
updateMany(filter: any, data: Entity) {
async updateMany(filter: any, data: Entity): Promise<number>{
this.stampEntity(data, false);
return mongoExecute(async ({ collection }) => {
return await collection?.updateMany(filter, { $set: data as any });
const result = await collection?.updateMany(filter, { $set: data as any });
return result?.modifiedCount || 0;
},
{ colName: this.collection });
}
replaceOne(filter: any, object: Entity) {
async replaceOne(filter: any, object: Entity): Promise<number> {
return mongoExecute(async ({collection}) => {
return await collection?.replaceOne(filter, object);
const result = await collection?.replaceOne(filter, object);
return result?.modifiedCount || 0;
}, {colName: this.collection});
}
aggregation(pipeline: any) {
async aggregation(pipeline: any): Promise<EntityMongo[]> {
return mongoExecute(
async ({ collection }) => {
return await collection?.aggregate(pipeline).toArray();
@ -108,7 +131,7 @@ export abstract class BaseMongoManager {
);
}
aggregationOne(pipeline: any) {
async aggregationOne(pipeline: any): Promise<EntityMongo | null> {
return mongoExecute(
async ({ collection }) => {
return await collection?.aggregate(pipeline).next();
@ -120,4 +143,11 @@ export abstract class BaseMongoManager {
protected toObjectId = (oid: string) => {
return toObjectId(oid);
};
protected stampEntity(entity: Entity, isCreate: boolean = true) {
if (isCreate) {
entity.createdAt = Date.now();
}
entity.modifiedAt = Date.now();
}
}

View File

@ -28,8 +28,8 @@ export class SessionManager extends ManagerBase {
SessionManager.sessions.set(session.id, session);
}
deleteSession(session: MatchSession) {
SessionManager.sessions.delete(session.id);
deleteSession(sessionId: string) {
SessionManager.sessions.delete(sessionId);
}
getSession(id: string) {

View File

@ -10,8 +10,10 @@ export default function(): Router {
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));
router.get('/match', authenticate({ roles: ['user']}), (req: Request, res: Response) => gameController.listMatches(req, res));
router.get('/match/:sessionId', authenticate({ roles: ['user']}), (req: Request, res: Response) => gameController.getMatch(req, res));
router.put('/match/:sessionId', authenticate({ roles: ['user']}), (req: Request, res: Response) => gameController.joinMatch(req, res));
router.delete('/match/:sessionId', authenticate({ roles: ['user']}), (req: Request, res: Response) => gameController.deleteMatch(req, res));
return router;
}

View File

@ -0,0 +1,110 @@
import PubSub from "pubsub-js";
import { EventActions } from "../../game/constants";
import { MatchSession } from "../../game/MatchSession";
import { SessionManager } from "../managers/SessionManager";
import { PlayerNotificationService } from "./PlayerNotificationService";
import { ServiceBase } from "./ServiceBase";
import { PlayerMove } from "../../game/entities/PlayerMove";
export class InteractionService extends ServiceBase{
private sessionManager: SessionManager = new SessionManager();
private notifyService = new PlayerNotificationService();
async handleClientEventWithAck(data: any): Promise<any> {
const { event, data: eventData } = data;
this.logger.trace(`Handling event: ${event} with ack`);
switch(event) {
case EventActions.START_SESSION:
return this.onStartSession(eventData);
default:
// PubSub.publish(event, eventData);
break;
}
}
handleClientEvent(data: any): any {
const { event, data: eventData } = data;
this.logger.trace(`Handling event: ${event}`);
switch(event) {
case 'client:player-move':
this.onClientMoveResponse(eventData);
break;
case 'client:set-client-ready':
this.onClientReady(eventData);
break;
case EventActions.PLAYER_READY:
this.onPlayerReady(eventData);
break;
default:
PubSub.publish(event, eventData);
break;
}
}
private onStartSession(data: any): any {
const sessionId: string = data.sessionId;
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.addPlayerToSession(session.createPlayerAI(i));
}
this.notifyService.sendEventToPlayers('server:match-starting', session.players);
session.start();
return {
status: 'ok'
};
}
}
public playerMove(data: any) {
this.onClientMoveResponse(data);
}
private onClientMoveResponse(data: any): any {
const { sessionId, move }: { sessionId: string, move: PlayerMove } = data;
const session: MatchSession | undefined = this.sessionManager.getSession(sessionId);
if (session !== undefined) {
session.playerMove(move);
return {
status: 'ok'
};
}
}
private onPlayerReady(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);
}
}
private onClientReady(data: any): any {
const { sessionId, userId } = data;
const session: MatchSession | undefined = this.sessionManager.getSession(sessionId);
session?.setClientReady(userId);
return {
status: 'ok'
}
}
public updateSocketId(sessionId: string, userId: string, socketId: string): any {
return this.sessionManager.updateSocketId(sessionId, userId, socketId);
}
}

View File

@ -1,35 +1,42 @@
import { DominoesGame } from "../../game/DominoesGame";
import { MatchSession } from "../../game/MatchSession";
import { NetworkClientNotifier } from "../../game/NetworkClientNotifier";
import { GameState } from "../../game/dto/GameState";
import { NetworkPlayer } from "../../game/entities/player/NetworkPlayer";
import { PlayerInterface } from "../../game/entities/player/PlayerInterface";
import { ServiceBase } from "./ServiceBase";
export class PlayerNotificationService {
export class PlayerNotificationService extends ServiceBase {
clientNotifier: NetworkClientNotifier = new NetworkClientNotifier();
async notifyGameState(game: DominoesGame) {
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);
players.map(player => player.sendEvent('update-game-state', gameState));
}
async notifyPlayersState(players: PlayerInterface[]) {
let promises: Promise<void>[] = players.map(player => player.sendEventWithAck('update-player-state', player.getState()));
return await Promise.all(promises);
notifyPlayersState(players: PlayerInterface[]) {
players.map(player => player.sendEvent('update-player-state', player.getState()));
}
async notifyMatchState(session: MatchSession) {
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);
players.map(player => player.sendEvent('update-match-session-state', session.getState()));
}
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 sendEventToPlayers(event: string, players: PlayerInterface[], data: Function | any = {}) {
players.forEach((player) => {
let dataTosend = data;
if (typeof data === 'function') {
dataTosend = data(player);
}
this.clientNotifier.sendEvent(player as NetworkPlayer, event, dataTosend);
});
}
async sendEvent(event: string, player: PlayerInterface, data: any = {}) {
player.sendEvent(event, data)
sendEvent(event: string, player: PlayerInterface, data: any = {}) {
this.logger.debug(`Sending event '${event}' to player ${player.id}`);
this.clientNotifier.sendEvent(player as NetworkPlayer, event, data);
}
}

View File

@ -1,15 +1,16 @@
import { SessionCreationError } from "../../common/errors/SessionCreationError";
import { SessionNotFoundError } from "../../common/errors/SessionNotFoundError";
import { wait, whileNotUndefined } from "../../common/utilities";
import { 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 { DbMatchSession, DbMatchSessionUpdate } from "../db/interfaces";
import { MatchSessionMongoManager } from "../db/mongo/MatchSessionMongoManager";
import { SessionManager } from "../managers/SessionManager";
import { ServiceBase } from "./ServiceBase";
import { SocketIoService } from "./SocketIoService";
import toObjectId from "../db/mongo/common/mongoUtils";
export class SessionService extends ServiceBase{
private dbManager: MatchSessionMongoManager = new MatchSessionMongoManager();
@ -37,68 +38,48 @@ export class SessionService extends ServiceBase{
this.sessionManager.setSession(session);
this.notifyService.notifyMatchState(session);
this.notifyService.notifyPlayersState(session.players);
this.logger.debug(`Session ${session.id} created`);
return session.id;
}
public async joinSession(user: any, sessionId: string): Promise<void> {
public async joinSession(user: any, sessionId: string): Promise<string> {
const session: MatchSession | undefined = this.sessionManager.getSession(sessionId);
if (session === undefined) {
throw new SessionNotFoundError();
} let socketClient;
}
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;
const player = new NetworkPlayer(user._id, user.username, socketClient.socketId);
this.dbManager.replaceOne({id: session.id}, matchSessionAdapter(session));
session.addPlayerToSession(player);
socketClient.sessionId = session.id;
this.notifyService.notifyMatchState(session);
this.notifyService.notifyPlayersState(session.players);
return sessionId
}
public listSessions(): Promise<DbMatchSession[]> {
return this.dbManager.listByFilter({});
public async listJoinableSessions(): Promise<DbMatchSession[]> {
return await this.dbManager.listByFilter(
{ state: 'created' },
{ createdAt: -1 },
{ page: 1, pageSize: 5 }) as DbMatchSession[];
}
public updateSocketId(sessionId: string, userId: string, socketId: string): any {
this.sessionManager.updateSocketId(sessionId, userId, socketId);
public async getSession(sessionId: string): Promise<DbMatchSession | undefined> {
return await this.dbManager.getById(sessionId) as DbMatchSession | undefined;
}
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 async deleteSession(sessionId: string): Promise<any> {
this.sessionManager.deleteSession(sessionId);
const session = {
_id: toObjectId(sessionId),
state: 'deleted'
} as DbMatchSessionUpdate;
return this.dbManager.update(session);
}
// public updateSession(session: MatchSession): any {

View File

@ -1,16 +1,16 @@
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";
import { InteractionService } from "./InteractionService";
import { ClientEvents } from "../../game/constants";
export class SocketIoService extends ServiceBase{
io: Server
private static clients: Map<string, any> = new Map();
private sessionService: SessionService = new SessionService();
private interactionService: InteractionService = new InteractionService();
static getClient(id: string) {
return this.clients.get(id);
@ -65,7 +65,7 @@ export class SocketIoService extends ServiceBase{
socket.join('room-general')
} else {
const client = SocketIoService.clients.get(userId);
this.sessionService.updateSocketId(client.sessionId, userId, socketId);
this.interactionService.updateSocketId(client.sessionId, userId, socketId);
client.socketId = socketId;
this.logger.debug(`User '${user.username}' already connected. Updating socketId to ${socketId}`);
client.alive = true;
@ -82,25 +82,14 @@ export class SocketIoService extends ServiceBase{
SocketIoService.clients.delete(id);
}
});
// socket.on('createSession', (data, callback) => {
// const response = sessionController.createSession(data, socket.id);
// callback(response);
// });
socket.on('startSession', (data, callback) => {
const response = this.sessionService.startSession(data);
callback(response);
socket.on(ClientEvents.CLIENT_EVENT, (data) => {
this.interactionService.handleClientEvent(data);
});
// socket.on('joinSession', (data, callback) => {
// const response = sessionController.joinSession(data, socket.id);
// callback(response);
// });
socket.on('playerReady', (data, callback) => {
const response = this.sessionService.setPlayerReady(data);
callback(response);
socket.on(ClientEvents.CLIENT_EVENT_WITH_ACK, (data, callback) => {
const result = this.interactionService.handleClientEventWithAck(data);
callback(result);
});
socket.on('pong', () => {
@ -110,11 +99,42 @@ export class SocketIoService extends ServiceBase{
SocketIoService.clients.set(id, {...client, alive: true });
}
})
socket.onAny((event, ...args) => {
if (['pong'].includes(event)) return;
let logStr = `Event received: ${event}`
if (event.startsWith('client:') && args.length > 0) {
logStr = `${logStr} (${args[0].event})`;
}
this.logger.debug(logStr);
});
this.pingClients()
});
// // socket.on('createSession', (data, callback) => {
// // const response = sessionController.createSession(data, socket.id);
// // callback(response);
// // });
// socket.on('startSession', (data, callback) => {
// const response = this.sessionService.startSession(data);
// callback(response);
// });
// socket.on('client:tile-animation-ended', (data) => {
// this.sessionService.onClientEndTileAnimation(data);
// });
// socket.on('joinSession', (data, callback) => {
// const response = sessionController.joinSession(data, socket.id);
// callback(response);
// });
// socket.on('playerReady', (data, callback) => {
// const response = this.sessionService.setPlayerReady(data);
// callback(response);
// });
});
}
private pingClients() {