import { Client } from "./index"; | |
import { EventEmitter } from "events"; | |
import Clock from "@gamestdio/timer"; | |
import { createTimeline, Timeline } from "@gamestdio/timeline"; | |
import { Protocol, send } from "./Protocol"; | |
import { logError, spliceOne } from "./Utils"; | |
export abstract class Room<T=any> extends EventEmitter { | |
// Abstract methods | |
abstract onMessage (client: Client, data: any): void; | |
abstract getNumClients(clients): number; | |
abstract mapClients(clients, fun); | |
abstract setState (newState); | |
protected abstract sendState (client: Client): void | |
protected abstract broadcastPatch (): boolean; | |
protected abstract addClient(client); | |
protected abstract removeClient(client): boolean; | |
protected abstract broadcast(data: any, ...args): boolean; | |
// vars that can be anything but NEED to be implemented | |
abstract state: any; | |
abstract clients: any; | |
// protected vars | |
protected _simulationInterval: NodeJS.Timer; | |
protected _patchInterval: NodeJS.Timer; | |
// Optional abstract methods | |
onInit? (options: any): void; | |
onJoin? (client: Client, options?: any): void | Promise<any>; | |
onLeave? (client: Client): void | Promise<any>; | |
onDispose? (): void | Promise<any>; | |
public clock: Clock = new Clock(); | |
public timeline?: Timeline; | |
public roomId: string; | |
public roomName: string; | |
public maxClients: number = Infinity; | |
public patchRate: number = 1000 / 20; // Default patch rate is 20fps (50ms) | |
public autoDispose: boolean = true; | |
// holds a list of clients with clientOptions during handshake | |
public connectingClients: {[clientId: string]: any} = {}; | |
public get maxClientsReached () { | |
return this.getNumClients(this.clients) + Object.keys(this.connectingClients).length >= this.maxClients; | |
} | |
public requestJoin (options: any, isNew?: boolean): number | boolean { | |
return 1; | |
} | |
public verifyClient (client: Client, options: any): boolean | Promise<any> { | |
return Promise.resolve(true); | |
} | |
public useTimeline ( maxSnapshots: number = 10 ): void { | |
this.timeline = createTimeline( maxSnapshots ); | |
} | |
public lock (): void { | |
this.emit('lock'); | |
} | |
public unlock (): void { | |
this.emit('unlock'); | |
} | |
public send (client: Client, data: any): void { | |
send(client, [ Protocol.ROOM_DATA, this.roomId, data ]); | |
} | |
public setPatchRate ( milliseconds: number ): void { | |
// clear previous interval in case called setPatchRate more than once | |
if ( this._patchInterval ) clearInterval(this._patchInterval); | |
this._patchInterval = setInterval( this.broadcastPatch.bind(this), milliseconds ); | |
} | |
public setSimulationInterval ( callback: Function, delay: number = 1000 / 60 ): void { | |
// clear previous interval in case called setSimulationInterval more than once | |
if ( this._simulationInterval ) clearInterval( this._simulationInterval ); | |
this._simulationInterval = setInterval( () => { | |
this.clock.tick(); | |
callback(); | |
}, delay ); | |
} | |
public disconnect (): Promise<any> { | |
let promises = []; | |
this.mapClients(this.clients, (client) => promises.push( this._onLeave(client) )) | |
return Promise.all(promises); | |
} | |
private _onJoin (client: Client, options?: any): void { | |
this.addClient(client); | |
// confirm room id that matches the room name requested to join | |
send(client, [ Protocol.JOIN_ROOM, client.sessionId ]); | |
// send current state when new client joins the room | |
if (this.state) { | |
this.sendState(client); | |
} | |
if (this.onJoin) { | |
this.onJoin(client, options); | |
} | |
} | |
private _onLeave (client: Client, isDisconnect: boolean = false): void | Promise<any> { | |
let userReturnData; | |
// | |
// call abstract 'onLeave' method only if the client has been successfully accepted. | |
// | |
// the '_onLeave' method may be called before 'verifyClient' succeeds, | |
// before the client is appended to `this.clients` | |
// | |
if (this.removeClient(client) && this.onLeave) { | |
userReturnData = this.onLeave(client); | |
} | |
this.emit('leave', client, isDisconnect); | |
// | |
// TODO: force disconnect from server. | |
// | |
// need to check why the connection is being re-directed to MatchMaking | |
// process after calling `client.close()` here | |
// | |
if (!isDisconnect) { | |
send(client, [ Protocol.LEAVE_ROOM, this.roomId ]); | |
} | |
// custom cleanup method & clear intervals | |
if ( this.autoDispose ) { | |
this._disposeIfEmpty(); | |
} | |
return userReturnData || Promise.resolve(); | |
} | |
protected _dispose (): Promise<any> { | |
let userReturnData; | |
if ( this.onDispose ) userReturnData = this.onDispose(); | |
if ( this._patchInterval ) clearInterval( this._patchInterval ); | |
if ( this._simulationInterval ) clearInterval( this._simulationInterval ); | |
// clear all timeouts/intervals + force to stop ticking | |
this.clock.clear(); | |
this.clock.stop(); | |
return userReturnData || Promise.resolve(); | |
} | |
protected _disposeIfEmpty () { | |
if ( this.getNumClients(this.clients) == 0 ) { | |
this._dispose(); | |
this.emit('dispose'); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment