2020-11-13 18:00:22 +01:00
|
|
|
/**
|
|
|
|
* 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.
|
|
|
|
*/
|
2021-06-24 10:09:10 +02:00
|
|
|
import { Zone, ZoneEventListener } from "./Zone";
|
|
|
|
import { ViewportInterface } from "_Model/Websocket/ViewportMessage";
|
|
|
|
import { ExSocketInterface } from "_Model/Websocket/ExSocketInterface";
|
2020-11-13 18:00:22 +01:00
|
|
|
//import Debug from "debug";
|
|
|
|
|
|
|
|
//const debug = Debug('positiondispatcher');
|
|
|
|
|
|
|
|
interface ZoneDescriptor {
|
|
|
|
i: number;
|
|
|
|
j: number;
|
|
|
|
}
|
|
|
|
|
|
|
|
export class PositionDispatcher {
|
2021-07-27 14:29:09 +02:00
|
|
|
// TODO: we need a way to clean the zones if no one is in the zone and no one listening (to free memory!)
|
2020-11-13 18:00:22 +01:00
|
|
|
|
|
|
|
private zones: Zone[][] = [];
|
|
|
|
|
2021-06-24 10:09:10 +02:00
|
|
|
constructor(
|
|
|
|
public readonly roomId: string,
|
|
|
|
private zoneWidth: number,
|
|
|
|
private zoneHeight: number,
|
|
|
|
private socketListener: ZoneEventListener
|
|
|
|
) {}
|
2020-11-13 18:00:22 +01:00
|
|
|
|
|
|
|
private getZoneDescriptorFromCoordinates(x: number, y: number): ZoneDescriptor {
|
|
|
|
return {
|
|
|
|
i: Math.floor(x / this.zoneWidth),
|
|
|
|
j: Math.floor(y / this.zoneHeight),
|
2021-06-24 10:09:10 +02:00
|
|
|
};
|
2020-11-13 18:00:22 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Sets the viewport coordinates.
|
|
|
|
*/
|
|
|
|
public setViewport(socket: ExSocketInterface, viewport: ViewportInterface): void {
|
|
|
|
if (viewport.left > viewport.right || viewport.top > viewport.bottom) {
|
2021-06-24 10:09:10 +02:00
|
|
|
console.warn("Invalid viewport received: ", viewport);
|
2020-11-13 18:00:22 +01:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const oldZones = socket.listenedZones;
|
|
|
|
const newZones = new Set<Zone>();
|
|
|
|
|
|
|
|
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));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-06-24 10:09:10 +02:00
|
|
|
const addedZones = [...newZones].filter((x) => !oldZones.has(x));
|
|
|
|
const removedZones = [...oldZones].filter((x) => !newZones.has(x));
|
2020-11-13 18:00:22 +01:00
|
|
|
|
|
|
|
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<Zone>();
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|