-
-
Save WebRTCGame/d1907430b9f109bc31505198015ec04a to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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