Skip to content

Instantly share code, notes, and snippets.

@sboli
Created May 22, 2020 12:58
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save sboli/97a7898ebb5205029a57120bcdf7fffa to your computer and use it in GitHub Desktop.
Save sboli/97a7898ebb5205029a57120bcdf7fffa to your computer and use it in GitHub Desktop.
import * as winston from 'winston';
import * as WebSocket from 'ws';
import Config from './config';
import Database from './database';
import GameEngine from './gameengine';
import {
ErrorCode,
EventType,
IBonus,
IEvent,
IFriendRequest,
IPlayer,
} from './networktypes';
import Server from './server';
import SocialEngine from './socialengine';
import Utils from './utils';
export default class ClientEventHandler {
constructor(
private server: Server,
private gameEngine: GameEngine,
private database: Database,
private socialEngine: SocialEngine
) { }
public handle(event: IEvent, client: WebSocket) {
return new Promise((resolve, reject) => {
if (event.type === EventType.LOGIN) {
this.performLogin(event, client).then(result => {
resolve(result);
});
} else if (event.type === EventType.CONFIGURATION) {
this.performGetConfiguration(event, client).then(result => {
resolve(result);
});
} else {
if (this.checkLogin(client, resolve)) {
switch (event.type) {
case EventType.JOIN:
this.performJoin(event, client).then(result => {
resolve(result);
});
break;
case EventType.HAND:
this.performGetHand(event, client).then(result => {
resolve(result);
});
break;
case EventType.PLAY:
this.performPlay(event, client).then(result => {
resolve(result);
});
break;
case EventType.LEADERBOARD:
this.performGetLeaderboard(event, client).then(result => {
resolve(result);
});
break;
case EventType.BONUS:
this.performGetBonus(event, client).then(result => {
resolve(result);
});
break;
case EventType.CHAT:
this.performChat(event, client).then(result => {
resolve();
});
break;
case EventType.FRIEND:
this.performFriend(event, client).then(result => {
resolve();
});
break;
case EventType.UNFRIEND:
this.performUnfriend(event, client).then(result => {
resolve();
});
break;
case EventType.PLAYER:
this.performGetPlayer(event, client).then(result => {
resolve(result);
});
break;
case EventType.UPDATE_PLAYER:
this.performUpdatePlayer(event, client).then(result => {
resolve();
});
break;
case EventType.CREATE_GAME:
this.gameEngine.create(event.data);
break;
case EventType.MY_GAMES:
this.gameEngine.getGames(
this.playerFromSocket(client),
event.data
);
break;
case EventType.DELETE_ACCOUNT:
this.deleteAccount(client);
break;
}
}
}
});
}
private deleteAccount(client: WebSocket) {
const player = this.playerFromSocket(client);
if (player && player._id) {
this.database.deletePlayer(player._id);
}
}
/**
* Gère l'évenement de connexion
* @param event L'évenement de connexion
* @param client La socket du client qui veut se connecter
*/
private async performLogin(event: IEvent, client: WebSocket): Promise<any> {
try {
const player = await this.gameEngine.login(event.data);
(client as any)._id = player._id;
this.server.registerClientSocket(player._id, client);
return { type: event.type, data: player };
} catch (err) {
return { type: EventType.ERROR, data: err };
}
}
/**
* Gère l'évènement pour rejoindre une partie
* @param event L'évènement pour rejoindre la partie
* @param client La socket du client
*/
private async performJoin(event: IEvent, client: WebSocket): Promise<any> {
try {
const player = await this.database.getPlayer((client as any)._id);
const result = await this.gameEngine.join(event.data ? event.data : undefined, player);
return { type: event.type, data: result };
} catch (err) {
return {
data: JSON.stringify(err),
type: EventType.ERROR
};
}
}
private async performGetHand(event: IEvent, client: WebSocket) {
const player = this.playerFromSocket(client);
if (event.data.player_id && event.data.player_id === player._id) {
this.gameEngine.getHand(event.data);
return undefined;
} else {
return Utils.makeError(
ErrorCode.BAD_REQUEST,
"Can't get someone else's hand"
);
}
}
private async performPlay(event: IEvent, client: WebSocket) {
return this.gameEngine.play(event.data);
}
private checkLogin(client: WebSocket, resolve: any) {
if ((client as any)._id) {
return true;
} else {
resolve(
Utils.makeError(
ErrorCode.NOT_LOGGED_IN,
'Unable to perform action. Please login first'
)
);
return false;
}
}
private async performGetLeaderboard(event: any, client: WebSocket) {
const players = await this.database.getLeaderboard(event.data.scope, event.data.page);
return {
data: players,
type: event.type
};
}
private async performGetConfiguration(event: any, client: WebSocket) {
return {
data: Config.getInstance().getSanitized(),
type: event.type
};
}
private performGetBonus(event: any, client: WebSocket) {
return new Promise((resolve, reject) => {
const id = (client as any)._id;
this.database.getPlayer(id).then(p => {
if (p) {
// Récupère les bonus existants
let existingBonuses: IBonus[] = [];
if (p.bonuses) {
existingBonuses = p.bonuses;
}
// Expire ceux qui sont expirés
for (const b of existingBonuses) {
const expectedEndTime =
b.parameters.install_time + b.parameters.duration;
let rem = expectedEndTime - Date.now();
if (rem < 0) {
rem = 0;
}
b.parameters.remaining_time = rem;
}
// Supprime les bonus expirés
const bonusesToKeep: IBonus[] = [];
for (const b of existingBonuses) {
if (b.parameters.remaining_time > 0) {
bonusesToKeep.push(b);
}
}
// demande l'installation d'un bonus
const newBonuses: IBonus[] = [];
if (event.data && event.data.length > 0) {
for (const b of event.data) {
if (b.name === 'points_x2') {
newBonuses.push({
name: b.name,
parameters: {
duration: Config.getInstance().get('bonus_duration'),
install_time: Date.now(),
},
});
} else if (b.name === 'points_x2_long') {
newBonuses.push({
name: 'points_x2', // b.name, // b.name,
parameters: {
duration: Config.getInstance().get('bonus_duration') * 4,
install_time: Date.now(),
},
});
}
}
}
// Merge les bonus existants avec les nouveaux bonus
for (const newBonus of newBonuses) {
let mustAdd = true;
for (const existingBonus of bonusesToKeep) {
if (existingBonus.name === newBonus.name) {
existingBonus.parameters.duration =
existingBonus.parameters.duration +
newBonus.parameters.duration;
mustAdd = false;
}
}
if (mustAdd === true) {
bonusesToKeep.push(newBonus);
}
}
for (const b of bonusesToKeep) {
const expectedEndTime =
b.parameters.install_time + b.parameters.duration;
let rem = expectedEndTime - Date.now();
if (rem < 0) {
rem = 0;
}
b.parameters.remaining_time = rem;
}
this.database.updatePlayer(id, { bonuses: bonusesToKeep });
resolve({ type: event.type, data: bonusesToKeep });
}
});
});
}
private async performChat(event: IEvent, client: WebSocket) {
if (event.data.from && event.data.message && event.data.game) {
this.gameEngine.chat(event.data);
}
}
private async performFriend(event: IEvent, client: WebSocket) {
const req = event.data as IFriendRequest;
if (req && req.from && req.mean && req._id && req.from !== req._id) {
this.socialEngine.friend(req);
}
}
private async performUnfriend(event: IEvent, client: WebSocket) {
const req = event.data as IFriendRequest;
if (req && req.from && req.mean && req._id) {
this.socialEngine.unfriend(req);
}
}
private async performGetPlayer(event: IEvent, client: WebSocket) {
if (event.data && event.data.length) {
const player = await this.database.getPlayer(event.data);
return { type: EventType.PLAYER, data: player };
}
}
private async performUpdatePlayer(event: IEvent, client: WebSocket) {
if (event.data && event.data) {
this.database.updatePlayer(this.playerFromSocket(client)._id, event.data);
}
}
private playerFromSocket(client: WebSocket): IPlayer {
return { _id: (client as any)._id };
}
}
import * as Http from 'http';
import { setTimeout } from 'timers';
import * as winston from 'winston';
import * as WebSocket from 'ws';
import ClientEventHandler from './clienteventhandler';
import Config from './config';
import Database from './database';
import GameEngine from './gameengine';
import Messaging from './messaging';
import { ErrorCode, EventType, IEvent } from './networktypes';
import ServerEventHandler from './servereventhandler';
import SocialEngine from './socialengine';
export default class Server {
private httpServer: Http.Server;
private server: WebSocket.Server;
private clientConnectionCheckerId: NodeJS.Timer;
private clientEventHandler: ClientEventHandler;
private serverEventHandler: ServerEventHandler;
private messaging: Messaging;
private gameEngine: GameEngine;
private database: Database;
private clients: Map<string, WebSocket>;
private socialEngine: SocialEngine;
constructor() {
this.messaging = new Messaging();
this.database = new Database();
this.clients = new Map<string, WebSocket>();
this.httpServer = Http.createServer();
this.server = new WebSocket.Server({
clientTracking: false,
path: Config.getInstance().get('path'),
server: this.httpServer,
});
this.serverEventHandler = new ServerEventHandler(
this,
this.database,
this.messaging
);
this.socialEngine = new SocialEngine(
this.database,
this.serverEventHandler
);
this.gameEngine = new GameEngine(
this.serverEventHandler,
this.database,
this.messaging
);
this.clientEventHandler = new ClientEventHandler(
this,
this.gameEngine,
this.database,
this.socialEngine
);
// Envoie un message de ping aux clients pour fermer la connexion en cas de problème non détecté
this.clientConnectionCheckerId = setInterval(() => {
winston.loggers.get('network').info('clients', this.clients.size);
const toDelete: string[] = [];
for (const e of this.clients.entries()) {
if ((e[1] as any).isAlive === false) {
e[1].terminate();
toDelete.push(e[0]);
}
(e[1] as any).isAlive = false;
e[1].ping('', false, (err: any) => {
// Erreur attendue ici. Ne pas traiter. Si le client est déco le ping va échouer
// winston.loggers.get('general').error('error while pinging');
});
}
if (toDelete.length !== 0) {
winston.loggers
.get('network')
.info('Terminating ' + toDelete.length + ' connections');
for (const d of toDelete) {
this.clients.delete(d);
}
}
}, Config.getInstance().get('clients_ping_interval'));
/*
exécuter le job régulièrement le job leaderboard.ts à la place
this.leaderboardBuilderId = setInterval(() => {
this.database.refreshLeaderboard();
}, Config.LEADERBOARD_UPDATE_INTERVAL);
// Met a jour la leaderboard au lancement du serveur
setTimeout(() => {
this.database.refreshLeaderboard();
}, 1000);
*/
this.server.on('connection', client => this.handleConnection(client));
this.server.on('close', client => this.handleClose(client));
this.server.on('error', () => {
winston.loggers.get('general').error('server error');
});
}
public getClientSocket(id: string): WebSocket {
return this.clients.get(id)!;
}
public registerClientSocket(id: string, socket: WebSocket) {
this.clients.set(id, socket);
}
/**
* Lance le serveur HTTP pour écouter les connexions websocket
* @param port Le port à écouter
*/
public run(port: number) {
this.httpServer.listen(port);
}
/**
* Les opérations à réaliser à la connexion d'un client
* @param client La socket client
*/
private handleConnection(client: WebSocket) {
(client as any).isAlive = true;
client.on('pong', () => ((client as any).isAlive = true));
client.on('message', message => this.handleMessage(client, message));
client.on('error', () => {
// On n'a rien à faire avec cette erreur
// winston.loggers.get('general').error('socket error');
});
}
/**
* Effectuer les opérations nécessaires lors de la déconnexion d'un client
* @param client La socket client à fermer
*/
private handleClose(client: WebSocket) {
const id = (client as any)._id;
if (id) {
this.clients.delete(id);
}
}
/**
* Gère la réception d'un message envoyé par le client
* @param client La socket client
* @param message Le message reçu
*/
private handleMessage(client: WebSocket, message: WebSocket.Data) {
winston.loggers.get('network').info('in', message);
let event: IEvent;
try {
event = JSON.parse(message as string);
} catch (e) {
winston.loggers.get('network').error('Malformated JSON : ' + e);
return;
}
this.clientEventHandler.handle(event, client).then(result => {
if (result) {
if (client.readyState === WebSocket.OPEN) {
const data = JSON.stringify(result);
winston.loggers.get('network').info('out', data);
client.send(data);
}
}
});
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment