Merge
This commit is contained in:
commit
3a9196fb82
37 changed files with 1601 additions and 213 deletions
|
@ -4,10 +4,10 @@ import * as http from "http";
|
|||
import {MessageUserPosition, Point} from "../Model/Websocket/MessageUserPosition"; //TODO fix import by "_Model/.."
|
||||
import {ExSocketInterface} from "../Model/Websocket/ExSocketInterface"; //TODO fix import by "_Model/.."
|
||||
import Jwt, {JsonWebTokenError} from "jsonwebtoken";
|
||||
import {SECRET_KEY, MINIMUM_DISTANCE, GROUP_RADIUS} from "../Enum/EnvironmentVariable"; //TODO fix import by "_Enum/..."
|
||||
import {SECRET_KEY, MINIMUM_DISTANCE, GROUP_RADIUS, ALLOW_ARTILLERY} from "../Enum/EnvironmentVariable"; //TODO fix import by "_Enum/..."
|
||||
import {World} from "../Model/World";
|
||||
import {Group} from "_Model/Group";
|
||||
import {UserInterface} from "_Model/UserInterface";
|
||||
import {Group} from "../Model/Group";
|
||||
import {UserInterface} from "../Model/UserInterface";
|
||||
import {isSetPlayerDetailsMessage,} from "../Model/Websocket/SetPlayerDetailsMessage";
|
||||
import {MessageUserJoined} from "../Model/Websocket/MessageUserJoined";
|
||||
import {MessageUserMoved} from "../Model/Websocket/MessageUserMoved";
|
||||
|
@ -19,12 +19,15 @@ import {isPointInterface, PointInterface} from "../Model/Websocket/PointInterfac
|
|||
import {isWebRtcSignalMessageInterface} from "../Model/Websocket/WebRtcSignalMessage";
|
||||
import {UserInGroupInterface} from "../Model/Websocket/UserInGroupInterface";
|
||||
import {isItemEventMessageInterface} from "../Model/Websocket/ItemEventMessage";
|
||||
import {uuid} from 'uuidv4';
|
||||
import {isUserMovesInterface} from "../Model/Websocket/UserMovesMessage";
|
||||
import {isViewport} from "../Model/Websocket/ViewportMessage";
|
||||
|
||||
enum SockerIoEvent {
|
||||
CONNECTION = "connection",
|
||||
DISCONNECT = "disconnect",
|
||||
JOIN_ROOM = "join-room", // bi-directional
|
||||
USER_POSITION = "user-position", // bi-directional
|
||||
USER_POSITION = "user-position", // From client to server
|
||||
USER_MOVED = "user-moved", // From server to client
|
||||
USER_LEFT = "user-left", // From server to client
|
||||
WEBRTC_SIGNAL = "webrtc-signal",
|
||||
|
@ -36,6 +39,21 @@ enum SockerIoEvent {
|
|||
GROUP_DELETE = "group-delete",
|
||||
SET_PLAYER_DETAILS = "set-player-details",
|
||||
ITEM_EVENT = 'item-event',
|
||||
SET_SILENT = "set_silent", // Set or unset the silent mode for this user.
|
||||
SET_VIEWPORT = "set-viewport",
|
||||
BATCH = "batch",
|
||||
}
|
||||
|
||||
function emitInBatch(socket: ExSocketInterface, event: string | symbol, payload: unknown): void {
|
||||
socket.batchedMessages.push({ event, payload});
|
||||
|
||||
if (socket.batchTimeout === null) {
|
||||
socket.batchTimeout = setTimeout(() => {
|
||||
socket.emit(SockerIoEvent.BATCH, socket.batchedMessages);
|
||||
socket.batchedMessages = [];
|
||||
socket.batchTimeout = null;
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
|
||||
export class IoSocketController {
|
||||
|
@ -61,10 +79,25 @@ export class IoSocketController {
|
|||
// Authentication with token. it will be decoded and stored in the socket.
|
||||
// Completely commented for now, as we do not use the "/login" route at all.
|
||||
this.Io.use((socket: Socket, next) => {
|
||||
console.log(socket.handshake.query.token);
|
||||
if (!socket.handshake.query || !socket.handshake.query.token) {
|
||||
console.error('An authentication error happened, a user tried to connect without a token.');
|
||||
return next(new Error('Authentication error'));
|
||||
}
|
||||
if(socket.handshake.query.token === 'test'){
|
||||
if (ALLOW_ARTILLERY) {
|
||||
(socket as ExSocketInterface).token = socket.handshake.query.token;
|
||||
(socket as ExSocketInterface).userId = uuid();
|
||||
(socket as ExSocketInterface).isArtillery = true;
|
||||
console.log((socket as ExSocketInterface).userId);
|
||||
next();
|
||||
return;
|
||||
} else {
|
||||
console.warn("In order to perform a load-testing test on this environment, you must set the ALLOW_ARTILLERY environment variable to 'true'");
|
||||
next();
|
||||
}
|
||||
}
|
||||
(socket as ExSocketInterface).isArtillery = false;
|
||||
if(this.searchClientByToken(socket.handshake.query.token)){
|
||||
console.error('An authentication error happened, a user tried to connect while its token is already connected.');
|
||||
return next(new Error('Authentication error'));
|
||||
|
@ -137,6 +170,11 @@ export class IoSocketController {
|
|||
ioConnection() {
|
||||
this.Io.on(SockerIoEvent.CONNECTION, (socket: Socket) => {
|
||||
const client : ExSocketInterface = socket as ExSocketInterface;
|
||||
client.batchedMessages = [];
|
||||
client.batchTimeout = null;
|
||||
client.emitInBatch = (event: string | symbol, payload: unknown): void => {
|
||||
emitInBatch(client, event, payload);
|
||||
}
|
||||
this.sockets.set(client.userId, client);
|
||||
|
||||
// Let's log server load when a user joins
|
||||
|
@ -156,6 +194,7 @@ export class IoSocketController {
|
|||
y: user y position on map
|
||||
*/
|
||||
socket.on(SockerIoEvent.JOIN_ROOM, (message: unknown, answerFn): void => {
|
||||
console.log(SockerIoEvent.JOIN_ROOM, message);
|
||||
try {
|
||||
if (!isJoinRoomMessageInterface(message)) {
|
||||
socket.emit(SockerIoEvent.MESSAGE_ERROR, {message: 'Invalid JOIN_ROOM message.'});
|
||||
|
@ -176,28 +215,30 @@ export class IoSocketController {
|
|||
//join new previous room
|
||||
const world = this.joinRoom(Client, roomId, message.position);
|
||||
|
||||
//add function to refresh position user in real time.
|
||||
//this.refreshUserPosition(Client);
|
||||
|
||||
const messageUserJoined = new MessageUserJoined(Client.userId, Client.name, Client.characterLayers, Client.position);
|
||||
|
||||
socket.to(roomId).emit(SockerIoEvent.JOIN_ROOM, messageUserJoined);
|
||||
|
||||
// The answer shall contain the list of all users of the room with their positions:
|
||||
const listOfUsers = Array.from(world.getUsers(), ([key, user]) => {
|
||||
const users = world.setViewport(Client, message.viewport);
|
||||
const listOfUsers = users.map((user: UserInterface) => {
|
||||
const player: ExSocketInterface|undefined = this.sockets.get(user.id);
|
||||
if (player === undefined) {
|
||||
console.warn('Something went wrong. The World contains a user "'+user.id+"' but this user does not exist in the sockets list!");
|
||||
return null;
|
||||
}
|
||||
return new MessageUserPosition(user.id, player.name, player.characterLayers, player.position);
|
||||
}).filter((item: MessageUserPosition|null) => item !== null);
|
||||
}, users);
|
||||
|
||||
const listOfItems: {[itemId: string]: unknown} = {};
|
||||
for (const [itemId, item] of world.getItemsState().entries()) {
|
||||
listOfItems[itemId] = item;
|
||||
}
|
||||
|
||||
//console.warn('ANSWER PLAYER POSITIONS', listOfUsers);
|
||||
if (answerFn === undefined && ALLOW_ARTILLERY === true) {
|
||||
/*console.error("TYPEOF answerFn", typeof(answerFn));
|
||||
console.error("answerFn", answerFn);
|
||||
process.exit(1)*/
|
||||
// For some reason, answerFn can be undefined if we use Artillery (?)
|
||||
return;
|
||||
}
|
||||
|
||||
answerFn({
|
||||
users: listOfUsers,
|
||||
items: listOfItems
|
||||
|
@ -208,28 +249,53 @@ export class IoSocketController {
|
|||
}
|
||||
});
|
||||
|
||||
socket.on(SockerIoEvent.USER_POSITION, (position: unknown): void => {
|
||||
socket.on(SockerIoEvent.SET_VIEWPORT, (message: unknown): void => {
|
||||
try {
|
||||
if (!isPointInterface(position)) {
|
||||
//console.log('SET_VIEWPORT')
|
||||
if (!isViewport(message)) {
|
||||
socket.emit(SockerIoEvent.MESSAGE_ERROR, {message: 'Invalid SET_VIEWPORT message.'});
|
||||
console.warn('Invalid SET_VIEWPORT message received: ', message);
|
||||
return;
|
||||
}
|
||||
|
||||
const Client = (socket as ExSocketInterface);
|
||||
Client.viewport = message;
|
||||
|
||||
const world = this.Worlds.get(Client.roomId);
|
||||
if (!world) {
|
||||
console.error("In SET_VIEWPORT, could not find world with id '", Client.roomId, "'");
|
||||
return;
|
||||
}
|
||||
world.setViewport(Client, Client.viewport);
|
||||
} catch (e) {
|
||||
console.error('An error occurred on "SET_VIEWPORT" event');
|
||||
console.error(e);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on(SockerIoEvent.USER_POSITION, (userMovesMessage: unknown): void => {
|
||||
//console.log(SockerIoEvent.USER_POSITION, userMovesMessage);
|
||||
try {
|
||||
if (!isUserMovesInterface(userMovesMessage)) {
|
||||
socket.emit(SockerIoEvent.MESSAGE_ERROR, {message: 'Invalid USER_POSITION message.'});
|
||||
console.warn('Invalid USER_POSITION message received: ', position);
|
||||
console.warn('Invalid USER_POSITION message received: ', userMovesMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
const Client = (socket as ExSocketInterface);
|
||||
|
||||
// sending to all clients in room except sender
|
||||
Client.position = position;
|
||||
Client.position = userMovesMessage.position;
|
||||
Client.viewport = userMovesMessage.viewport;
|
||||
|
||||
// update position in the world
|
||||
const world = this.Worlds.get(Client.roomId);
|
||||
if (!world) {
|
||||
console.error("Could not find world with id '", Client.roomId, "'");
|
||||
console.error("In USER_POSITION, could not find world with id '", Client.roomId, "'");
|
||||
return;
|
||||
}
|
||||
world.updatePosition(Client, position);
|
||||
|
||||
socket.to(Client.roomId).emit(SockerIoEvent.USER_MOVED, new MessageUserMoved(Client.userId, Client.position));
|
||||
world.updatePosition(Client, Client.position);
|
||||
world.setViewport(Client, Client.viewport);
|
||||
} catch (e) {
|
||||
console.error('An error occurred on "user_position" event');
|
||||
console.error(e);
|
||||
|
@ -275,6 +341,7 @@ export class IoSocketController {
|
|||
|
||||
// Let's send the user id to the user
|
||||
socket.on(SockerIoEvent.SET_PLAYER_DETAILS, (playerDetails: unknown, answerFn) => {
|
||||
console.log(SockerIoEvent.SET_PLAYER_DETAILS, playerDetails);
|
||||
if (!isSetPlayerDetailsMessage(playerDetails)) {
|
||||
socket.emit(SockerIoEvent.MESSAGE_ERROR, {message: 'Invalid SET_PLAYER_DETAILS message.'});
|
||||
console.warn('Invalid SET_PLAYER_DETAILS message received: ', playerDetails);
|
||||
|
@ -283,7 +350,34 @@ export class IoSocketController {
|
|||
const Client = (socket as ExSocketInterface);
|
||||
Client.name = playerDetails.name;
|
||||
Client.characterLayers = playerDetails.characterLayers;
|
||||
answerFn(Client.userId);
|
||||
// Artillery fails when receiving an acknowledgement that is not a JSON object
|
||||
if (!Client.isArtillery) {
|
||||
answerFn(Client.userId);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on(SockerIoEvent.SET_SILENT, (silent: unknown) => {
|
||||
console.log(SockerIoEvent.SET_SILENT, silent);
|
||||
if (typeof silent !== "boolean") {
|
||||
socket.emit(SockerIoEvent.MESSAGE_ERROR, {message: 'Invalid SET_SILENT message.'});
|
||||
console.warn('Invalid SET_SILENT message received: ', silent);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const Client = (socket as ExSocketInterface);
|
||||
|
||||
// update position in the world
|
||||
const world = this.Worlds.get(Client.roomId);
|
||||
if (!world) {
|
||||
console.error("In SET_SILENT, could not find world with id '", Client.roomId, "'");
|
||||
return;
|
||||
}
|
||||
world.setSilent(Client, silent);
|
||||
} catch (e) {
|
||||
console.error('An error occurred on "SET_SILENT"');
|
||||
console.error(e);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on(SockerIoEvent.ITEM_EVENT, (itemEvent: unknown) => {
|
||||
|
@ -359,8 +453,6 @@ export class IoSocketController {
|
|||
// leave previous room and world
|
||||
if(Client.roomId){
|
||||
try {
|
||||
Client.to(Client.roomId).emit(SockerIoEvent.USER_LEFT, Client.userId);
|
||||
|
||||
//user leave previous world
|
||||
const world: World | undefined = this.Worlds.get(Client.roomId);
|
||||
if (world) {
|
||||
|
@ -396,6 +488,25 @@ export class IoSocketController {
|
|||
this.sendUpdateGroupEvent(group);
|
||||
}, (groupUuid: string, lastUser: UserInterface) => {
|
||||
this.sendDeleteGroupEvent(groupUuid, lastUser);
|
||||
}, (user, listener) => {
|
||||
const clientUser = this.searchClientByIdOrFail(user.id);
|
||||
const clientListener = this.searchClientByIdOrFail(listener.id);
|
||||
const messageUserJoined = new MessageUserJoined(clientUser.userId, clientUser.name, clientUser.characterLayers, clientUser.position);
|
||||
|
||||
clientListener.emit(SockerIoEvent.JOIN_ROOM, messageUserJoined);
|
||||
//console.log("Sending JOIN_ROOM event");
|
||||
}, (user, position, listener) => {
|
||||
const clientUser = this.searchClientByIdOrFail(user.id);
|
||||
const clientListener = this.searchClientByIdOrFail(listener.id);
|
||||
|
||||
clientListener.emitInBatch(SockerIoEvent.USER_MOVED, new MessageUserMoved(clientUser.userId, clientUser.position));
|
||||
//console.log("Sending USER_MOVED event");
|
||||
}, (user, listener) => {
|
||||
const clientUser = this.searchClientByIdOrFail(user.id);
|
||||
const clientListener = this.searchClientByIdOrFail(listener.id);
|
||||
|
||||
clientListener.emit(SockerIoEvent.USER_LEFT, clientUser.userId);
|
||||
//console.log("Sending USER_LEFT event");
|
||||
});
|
||||
this.Worlds.set(roomId, world);
|
||||
}
|
||||
|
|
|
@ -2,10 +2,12 @@ const SECRET_KEY = process.env.SECRET_KEY || "THECODINGMACHINE_SECRET_KEY";
|
|||
const URL_ROOM_STARTED = "/Floor0/floor0.json";
|
||||
const MINIMUM_DISTANCE = process.env.MINIMUM_DISTANCE ? Number(process.env.MINIMUM_DISTANCE) : 64;
|
||||
const GROUP_RADIUS = process.env.GROUP_RADIUS ? Number(process.env.GROUP_RADIUS) : 48;
|
||||
const ALLOW_ARTILLERY = process.env.ALLOW_ARTILLERY ? process.env.ALLOW_ARTILLERY == 'true' : false;
|
||||
|
||||
export {
|
||||
SECRET_KEY,
|
||||
URL_ROOM_STARTED,
|
||||
MINIMUM_DISTANCE,
|
||||
GROUP_RADIUS
|
||||
GROUP_RADIUS,
|
||||
ALLOW_ARTILLERY
|
||||
}
|
||||
|
|
120
back/src/Model/PositionNotifier.ts
Normal file
120
back/src/Model/PositionNotifier.ts
Normal file
|
@ -0,0 +1,120 @@
|
|||
/**
|
||||
* 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 {UserEntersCallback, UserLeavesCallback, UserMovesCallback, Zone} from "./Zone";
|
||||
import {PointInterface} from "_Model/Websocket/PointInterface";
|
||||
import {UserInterface} from "_Model/UserInterface";
|
||||
import {ViewportInterface} from "_Model/Websocket/ViewportMessage";
|
||||
|
||||
interface ZoneDescriptor {
|
||||
i: number;
|
||||
j: number;
|
||||
}
|
||||
|
||||
export class PositionNotifier {
|
||||
|
||||
// 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(private zoneWidth: number, private zoneHeight: number, private onUserEnters: UserEntersCallback, private onUserMoves: UserMovesCallback, private onUserLeaves: UserLeavesCallback) {
|
||||
}
|
||||
|
||||
private getZoneDescriptorFromCoordinates(x: number, y: number): ZoneDescriptor {
|
||||
return {
|
||||
i: Math.floor(x / this.zoneWidth),
|
||||
j: Math.floor(y / this.zoneHeight),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the viewport coordinates.
|
||||
* Returns the list of new users to add
|
||||
*/
|
||||
public setViewport(user: UserInterface, viewport: ViewportInterface): UserInterface[] {
|
||||
if (viewport.left > viewport.right || viewport.top > viewport.bottom) {
|
||||
console.warn('Invalid viewport received: ', viewport);
|
||||
return [];
|
||||
}
|
||||
|
||||
const oldZones = user.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));
|
||||
}
|
||||
}
|
||||
|
||||
const addedZones = [...newZones].filter(x => !oldZones.has(x));
|
||||
const removedZones = [...oldZones].filter(x => !newZones.has(x));
|
||||
|
||||
|
||||
let users: UserInterface[] = [];
|
||||
for (const zone of addedZones) {
|
||||
zone.startListening(user);
|
||||
users = users.concat(Array.from(zone.getPlayers()))
|
||||
}
|
||||
for (const zone of removedZones) {
|
||||
zone.stopListening(user);
|
||||
}
|
||||
|
||||
return users;
|
||||
}
|
||||
|
||||
public updatePosition(user: UserInterface, userPosition: PointInterface): void {
|
||||
// Did we change zone?
|
||||
const oldZoneDesc = this.getZoneDescriptorFromCoordinates(user.position.x, user.position.y);
|
||||
const newZoneDesc = this.getZoneDescriptorFromCoordinates(userPosition.x, userPosition.y);
|
||||
|
||||
if (oldZoneDesc.i != newZoneDesc.i || oldZoneDesc.j != newZoneDesc.j) {
|
||||
const oldZone = this.getZone(oldZoneDesc.i, oldZoneDesc.j);
|
||||
const newZone = this.getZone(newZoneDesc.i, newZoneDesc.j);
|
||||
|
||||
// Leave old zone
|
||||
oldZone.leave(user, newZone);
|
||||
|
||||
// Enter new zone
|
||||
newZone.enter(user, oldZone, userPosition);
|
||||
} else {
|
||||
const zone = this.getZone(oldZoneDesc.i, oldZoneDesc.j);
|
||||
zone.move(user, userPosition);
|
||||
}
|
||||
}
|
||||
|
||||
public leave(user: UserInterface): void {
|
||||
const oldZoneDesc = this.getZoneDescriptorFromCoordinates(user.position.x, user.position.y);
|
||||
const oldZone = this.getZone(oldZoneDesc.i, oldZoneDesc.j);
|
||||
oldZone.leave(user, null);
|
||||
|
||||
// Also, let's stop listening on viewports
|
||||
for (const zone of user.listenedZones) {
|
||||
zone.stopListening(user);
|
||||
}
|
||||
}
|
||||
|
||||
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.onUserEnters, this.onUserMoves, this.onUserLeaves);
|
||||
this.zones[j][i] = zone;
|
||||
}
|
||||
return zone;
|
||||
}
|
||||
}
|
|
@ -1,8 +1,11 @@
|
|||
import { Group } from "./Group";
|
||||
import { PointInterface } from "./Websocket/PointInterface";
|
||||
import {Zone} from "_Model/Zone";
|
||||
|
||||
export interface UserInterface {
|
||||
id: string,
|
||||
group?: Group,
|
||||
position: PointInterface
|
||||
}
|
||||
position: PointInterface,
|
||||
silent: boolean,
|
||||
listenedZones: Set<Zone>
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ import {Socket} from "socket.io";
|
|||
import {PointInterface} from "./PointInterface";
|
||||
import {Identificable} from "./Identificable";
|
||||
import {TokenInterface} from "../../Controller/AuthenticateController";
|
||||
import {ViewportInterface} from "_Model/Websocket/ViewportMessage";
|
||||
|
||||
export interface ExSocketInterface extends Socket, Identificable {
|
||||
token: string;
|
||||
|
@ -11,4 +12,12 @@ export interface ExSocketInterface extends Socket, Identificable {
|
|||
name: string;
|
||||
characterLayers: string[];
|
||||
position: PointInterface;
|
||||
viewport: ViewportInterface;
|
||||
isArtillery: boolean; // Whether this socket is opened by Artillery for load testing (hack)
|
||||
/**
|
||||
* Pushes an event that will be sent in the next batch of events
|
||||
*/
|
||||
emitInBatch: (event: string | symbol, payload: unknown) => void;
|
||||
batchedMessages: Array<{ event: string | symbol, payload: unknown }>;
|
||||
batchTimeout: NodeJS.Timeout|null;
|
||||
}
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import * as tg from "generic-type-guard";
|
||||
import {isPointInterface} from "./PointInterface";
|
||||
import {isViewport} from "./ViewportMessage";
|
||||
|
||||
export const isJoinRoomMessageInterface =
|
||||
new tg.IsInterface().withProperties({
|
||||
roomId: tg.isString,
|
||||
position: isPointInterface,
|
||||
viewport: isViewport
|
||||
}).get();
|
||||
export type JoinRoomMessageInterface = tg.GuardedType<typeof isJoinRoomMessageInterface>;
|
||||
|
|
11
back/src/Model/Websocket/UserMovesMessage.ts
Normal file
11
back/src/Model/Websocket/UserMovesMessage.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
import * as tg from "generic-type-guard";
|
||||
import {isPointInterface} from "./PointInterface";
|
||||
import {isViewport} from "./ViewportMessage";
|
||||
|
||||
|
||||
export const isUserMovesInterface =
|
||||
new tg.IsInterface().withProperties({
|
||||
position: isPointInterface,
|
||||
viewport: isViewport,
|
||||
}).get();
|
||||
export type UserMovesInterface = tg.GuardedType<typeof isUserMovesInterface>;
|
10
back/src/Model/Websocket/ViewportMessage.ts
Normal file
10
back/src/Model/Websocket/ViewportMessage.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
import * as tg from "generic-type-guard";
|
||||
|
||||
export const isViewport =
|
||||
new tg.IsInterface().withProperties({
|
||||
left: tg.isNumber,
|
||||
top: tg.isNumber,
|
||||
right: tg.isNumber,
|
||||
bottom: tg.isNumber,
|
||||
}).get();
|
||||
export type ViewportInterface = tg.GuardedType<typeof isViewport>;
|
|
@ -6,6 +6,9 @@ import {UserInterface} from "./UserInterface";
|
|||
import {ExSocketInterface} from "_Model/Websocket/ExSocketInterface";
|
||||
import {PositionInterface} from "_Model/PositionInterface";
|
||||
import {Identificable} from "_Model/Websocket/Identificable";
|
||||
import {UserEntersCallback, UserLeavesCallback, UserMovesCallback, Zone} from "_Model/Zone";
|
||||
import {PositionNotifier} from "./PositionNotifier";
|
||||
import {ViewportInterface} from "_Model/Websocket/ViewportMessage";
|
||||
|
||||
export type ConnectCallback = (user: string, group: Group) => void;
|
||||
export type DisconnectCallback = (user: string, group: Group) => void;
|
||||
|
@ -29,12 +32,17 @@ export class World {
|
|||
|
||||
private itemsState: Map<number, unknown> = new Map<number, unknown>();
|
||||
|
||||
private readonly positionNotifier: PositionNotifier;
|
||||
|
||||
constructor(connectCallback: ConnectCallback,
|
||||
disconnectCallback: DisconnectCallback,
|
||||
minDistance: number,
|
||||
groupRadius: number,
|
||||
groupUpdatedCallback: GroupUpdatedCallback,
|
||||
groupDeletedCallback: GroupDeletedCallback)
|
||||
groupDeletedCallback: GroupDeletedCallback,
|
||||
onUserEnters: UserEntersCallback,
|
||||
onUserMoves: UserMovesCallback,
|
||||
onUserLeaves: UserLeavesCallback)
|
||||
{
|
||||
this.users = new Map<string, UserInterface>();
|
||||
this.groups = new Set<Group>();
|
||||
|
@ -44,6 +52,8 @@ export class World {
|
|||
this.groupRadius = groupRadius;
|
||||
this.groupUpdatedCallback = groupUpdatedCallback;
|
||||
this.groupDeletedCallback = groupDeletedCallback;
|
||||
// A zone is 10 sprites wide.
|
||||
this.positionNotifier = new PositionNotifier(320, 320, onUserEnters, onUserMoves, onUserLeaves);
|
||||
}
|
||||
|
||||
public getGroups(): Group[] {
|
||||
|
@ -57,7 +67,9 @@ export class World {
|
|||
public join(socket : Identificable, userPosition: PointInterface): void {
|
||||
this.users.set(socket.userId, {
|
||||
id: socket.userId,
|
||||
position: userPosition
|
||||
position: userPosition,
|
||||
silent: false, // FIXME: silent should be set at the correct value when joining a room.
|
||||
listenedZones: new Set<Zone>()
|
||||
});
|
||||
// Let's call update position to trigger the join / leave room
|
||||
this.updatePosition(socket, userPosition);
|
||||
|
@ -72,6 +84,10 @@ export class World {
|
|||
this.leaveGroup(userObj);
|
||||
}
|
||||
this.users.delete(user.userId);
|
||||
|
||||
if (userObj !== undefined) {
|
||||
this.positionNotifier.leave(userObj);
|
||||
}
|
||||
}
|
||||
|
||||
public isEmpty(): boolean {
|
||||
|
@ -84,8 +100,14 @@ export class World {
|
|||
return;
|
||||
}
|
||||
|
||||
this.positionNotifier.updatePosition(user, userPosition);
|
||||
|
||||
user.position = userPosition;
|
||||
|
||||
if (user.silent) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof user.group === 'undefined') {
|
||||
// If the user is not part of a group:
|
||||
// should he join a group?
|
||||
|
@ -120,6 +142,26 @@ export class World {
|
|||
}
|
||||
}
|
||||
|
||||
setSilent(socket: Identificable, silent: boolean) {
|
||||
const user = this.users.get(socket.userId);
|
||||
if(typeof user === 'undefined') {
|
||||
console.warn('In setSilent, could not find user with ID "'+socket.userId+'" in world.');
|
||||
return;
|
||||
}
|
||||
if (user.silent === silent) {
|
||||
return;
|
||||
}
|
||||
|
||||
user.silent = silent;
|
||||
if (silent && user.group !== undefined) {
|
||||
this.leaveGroup(user);
|
||||
}
|
||||
if (!silent) {
|
||||
// If we are back to life, let's trigger a position update to see if we can join some group.
|
||||
this.updatePosition(socket, user.position);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a user leave a group and closes and destroy the group if the group contains only one remaining person.
|
||||
*
|
||||
|
@ -147,6 +189,7 @@ export class World {
|
|||
* Looks for the closest user that is:
|
||||
* - close enough (distance <= minDistance)
|
||||
* - not in a group
|
||||
* - not silent
|
||||
* OR
|
||||
* - close enough to a group (distance <= groupRadius)
|
||||
*/
|
||||
|
@ -162,6 +205,9 @@ export class World {
|
|||
if(currentUser === user) {
|
||||
return;
|
||||
}
|
||||
if (currentUser.silent) {
|
||||
return;
|
||||
}
|
||||
|
||||
const distance = World.computeDistance(user, currentUser); // compute distance between peers.
|
||||
|
||||
|
@ -297,4 +343,12 @@ export class World {
|
|||
}
|
||||
return 0;
|
||||
}*/
|
||||
setViewport(socket : Identificable, viewport: ViewportInterface): UserInterface[] {
|
||||
const user = this.users.get(socket.userId);
|
||||
if(typeof user === 'undefined') {
|
||||
console.warn('In setViewport, could not find user with ID "'+socket.userId+'" in world.');
|
||||
return [];
|
||||
}
|
||||
return this.positionNotifier.setViewport(user, viewport);
|
||||
}
|
||||
}
|
||||
|
|
96
back/src/Model/Zone.ts
Normal file
96
back/src/Model/Zone.ts
Normal file
|
@ -0,0 +1,96 @@
|
|||
import {UserInterface} from "./UserInterface";
|
||||
import {PointInterface} from "_Model/Websocket/PointInterface";
|
||||
import {PositionInterface} from "_Model/PositionInterface";
|
||||
|
||||
export type UserEntersCallback = (user: UserInterface, listener: UserInterface) => void;
|
||||
export type UserMovesCallback = (user: UserInterface, position: PointInterface, listener: UserInterface) => void;
|
||||
export type UserLeavesCallback = (user: UserInterface, listener: UserInterface) => void;
|
||||
|
||||
export class Zone {
|
||||
private players: Set<UserInterface> = new Set<UserInterface>();
|
||||
private listeners: Set<UserInterface> = new Set<UserInterface>();
|
||||
|
||||
constructor(private onUserEnters: UserEntersCallback, private onUserMoves: UserMovesCallback, private onUserLeaves: UserLeavesCallback) {
|
||||
}
|
||||
|
||||
/**
|
||||
* A user leaves the zone
|
||||
*/
|
||||
public leave(user: UserInterface, newZone: Zone|null) {
|
||||
this.players.delete(user);
|
||||
this.notifyUserLeft(user, newZone);
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify listeners of this zone that this user left
|
||||
*/
|
||||
private notifyUserLeft(user: UserInterface, newZone: Zone|null) {
|
||||
for (const listener of this.listeners) {
|
||||
if (listener !== user && (newZone === null || !listener.listenedZones.has(newZone))) {
|
||||
this.onUserLeaves(user, listener);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enter(user: UserInterface, oldZone: Zone|null, position: PointInterface) {
|
||||
this.players.add(user);
|
||||
this.notifyUserEnter(user, oldZone, position);
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify listeners of this zone that this user entered
|
||||
*/
|
||||
private notifyUserEnter(user: UserInterface, oldZone: Zone|null, position: PointInterface) {
|
||||
for (const listener of this.listeners) {
|
||||
if (listener === user) {
|
||||
continue;
|
||||
}
|
||||
if (oldZone === null || !listener.listenedZones.has(oldZone)) {
|
||||
this.onUserEnters(user, listener);
|
||||
} else {
|
||||
this.onUserMoves(user, position, listener);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public move(user: UserInterface, position: PointInterface) {
|
||||
if (!this.players.has(user)) {
|
||||
this.players.add(user);
|
||||
const foo = this.players;
|
||||
this.notifyUserEnter(user, null, position);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const listener of this.listeners) {
|
||||
if (listener !== user) {
|
||||
this.onUserMoves(user,position, listener);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public startListening(listener: UserInterface): void {
|
||||
for (const player of this.players) {
|
||||
if (player !== listener) {
|
||||
this.onUserEnters(player, listener);
|
||||
}
|
||||
}
|
||||
|
||||
this.listeners.add(listener);
|
||||
listener.listenedZones.add(this);
|
||||
}
|
||||
|
||||
public stopListening(listener: UserInterface): void {
|
||||
for (const player of this.players) {
|
||||
if (player !== listener) {
|
||||
this.onUserLeaves(player, listener);
|
||||
}
|
||||
}
|
||||
|
||||
this.listeners.delete(listener);
|
||||
listener.listenedZones.delete(this);
|
||||
}
|
||||
|
||||
public getPlayers(): Set<UserInterface> {
|
||||
return this.players;
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue