/** * Tracks the position of every player on the map, and sends notifications to the players interested in knowing about the move * (i.e. players that are looking at the zone the player is currently in) * * Internally, the PositionNotifier works with Zones. A zone is a square area of a map. * Each player is in a given zone, and each player tracks one or many zones (depending on the player viewport) * * The PositionNotifier is important for performance. It allows us to send the position of players only to a restricted * number of players around the current player. */ import {Zone, ZoneEventListener} from "./Zone"; import {ViewportInterface} from "_Model/Websocket/ViewportMessage"; import {ExSocketInterface} from "_Model/Websocket/ExSocketInterface"; //import Debug from "debug"; //const debug = Debug('positiondispatcher'); interface ZoneDescriptor { i: number; j: number; } export class PositionDispatcher { // TODO: we need a way to clean the zones if noone is in the zone and noone listening (to free memory!) private zones: Zone[][] = []; constructor(public readonly roomId: string, private zoneWidth: number, private zoneHeight: number, private socketListener: ZoneEventListener) { } private getZoneDescriptorFromCoordinates(x: number, y: number): ZoneDescriptor { return { i: Math.floor(x / this.zoneWidth), j: Math.floor(y / this.zoneHeight), } } /** * Sets the viewport coordinates. */ public setViewport(socket: ExSocketInterface, viewport: ViewportInterface): void { if (viewport.left > viewport.right || viewport.top > viewport.bottom) { console.warn('Invalid viewport received: ', viewport); return; } const oldZones = socket.listenedZones; const newZones = new Set(); const topLeftDesc = this.getZoneDescriptorFromCoordinates(viewport.left, viewport.top); const bottomRightDesc = this.getZoneDescriptorFromCoordinates(viewport.right, viewport.bottom); for (let j = topLeftDesc.j; j <= bottomRightDesc.j; j++) { for (let i = topLeftDesc.i; i <= bottomRightDesc.i; i++) { newZones.add(this.getZone(i, j)); } } const addedZones = [...newZones].filter(x => !oldZones.has(x)); const removedZones = [...oldZones].filter(x => !newZones.has(x)); for (const zone of addedZones) { zone.startListening(socket); } for (const zone of removedZones) { this.stopListening(zone, socket); } } private stopListening(zone: Zone, socket: ExSocketInterface): void { zone.stopListening(socket); if (!zone.hasListeners()) { zone.close(); this.deleteZone(zone); } } /** * Removes the zone from the dispatcher. * Warning, zone is not closed by this method. */ private deleteZone(zone: Zone): void { delete this.zones[zone.y][zone.x]; if (Object.keys(this.zones[zone.y]).length === 0) { delete this.zones[zone.y]; } } public removeViewport(socket: ExSocketInterface): void { // Also, let's stop listening on viewports for (const zone of socket.listenedZones) { this.stopListening(zone, socket); } } public isEmpty(): boolean { return Object.keys(this.zones).length === 0; } private getZone(i: number, j: number): Zone { let zoneRow = this.zones[j]; if (zoneRow === undefined) { zoneRow = new Array(); this.zones[j] = zoneRow; } let zone = this.zones[j][i]; if (zone === undefined) { zone = new Zone(this, this.socketListener, i, j, (e, myZone) => { // On failure, we delete the zone from the dispatcher so it can be recreated later. this.deleteZone(myZone); // TODO: we should check if the position dispatcher is still containing a room and propagate the onFailure to the parent if it is empty. }); zone.init(); this.zones[j][i] = zone; } return zone; } }