partey_workadventure/back/src/Controller/IoSocketController.ts

929 lines
37 KiB
TypeScript
Raw Normal View History

import * as http from "http";
2020-05-15 23:24:04 +02:00
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";
2020-10-13 09:37:38 +02:00
import {
SECRET_KEY,
MINIMUM_DISTANCE,
GROUP_RADIUS,
ALLOW_ARTILLERY,
ADMIN_API_URL,
ADMIN_API_TOKEN
} from "../Enum/EnvironmentVariable"; //TODO fix import by "_Enum/..."
2020-04-27 00:44:25 +02:00
import {World} from "../Model/World";
import {Group} from "../Model/Group";
2020-09-16 16:06:43 +02:00
import {User} from "../Model/User";
import {isSetPlayerDetailsMessage,} from "../Model/Websocket/SetPlayerDetailsMessage";
import {MessageUserJoined} from "../Model/Websocket/MessageUserJoined";
import si from "systeminformation";
import {Gauge} from "prom-client";
import {TokenInterface} from "../Controller/AuthenticateController";
import {isJoinRoomMessageInterface} from "../Model/Websocket/JoinRoomMessage";
2020-09-21 11:24:03 +02:00
import {PointInterface} from "../Model/Websocket/PointInterface";
import {isWebRtcSignalMessageInterface} from "../Model/Websocket/WebRtcSignalMessage";
import {UserInGroupInterface} from "../Model/Websocket/UserInGroupInterface";
2020-07-27 22:36:07 +02:00
import {isItemEventMessageInterface} from "../Model/Websocket/ItemEventMessage";
2020-10-01 16:51:51 +02:00
import { v4 as uuidv4 } from 'uuid';
2020-09-16 16:06:43 +02:00
import {GroupUpdateInterface} from "_Model/Websocket/GroupUpdateInterface";
import {Movable} from "../Model/Movable";
import {
PositionMessage,
SetPlayerDetailsMessage,
SubMessage,
UserMovedMessage,
2020-09-24 17:24:37 +02:00
BatchMessage,
GroupUpdateMessage,
PointMessage,
GroupDeleteMessage,
UserJoinedMessage,
UserLeftMessage,
2020-09-28 18:52:54 +02:00
ItemEventMessage,
ViewportMessage,
ClientToServerMessage,
JoinRoomMessage,
ErrorMessage,
RoomJoinedMessage,
ItemStateMessage,
2020-09-29 16:01:22 +02:00
ServerToClientMessage,
SetUserIdMessage,
SilentMessage,
WebRtcSignalToClientMessage,
WebRtcSignalToServerMessage,
WebRtcStartMessage, WebRtcDisconnectMessage, PlayGlobalMessage, ReportPlayerMessage, TeleportMessageMessage
} from "../Messages/generated/messages_pb";
import {UserMovesMessage} from "../Messages/generated/messages_pb";
import Direction = PositionMessage.Direction;
import {ProtobufUtils} from "../Model/Websocket/ProtobufUtils";
import {App, HttpRequest, TemplatedApp, WebSocket} from "uWebSockets.js"
import {parse} from "query-string";
import {cpuTracker} from "../Services/CpuTracker";
2020-10-13 09:37:38 +02:00
import Axios from "axios";
2020-04-29 01:40:32 +02:00
2020-09-28 18:52:54 +02:00
function emitInBatch(socket: ExSocketInterface, payload: SubMessage): void {
socket.batchedMessages.addPayload(payload);
if (socket.batchTimeout === null) {
socket.batchTimeout = setTimeout(() => {
2020-09-29 09:45:47 +02:00
if (socket.disconnecting) {
return;
}
2020-09-28 18:52:54 +02:00
const serverToClientMessage = new ServerToClientMessage();
serverToClientMessage.setBatchmessage(socket.batchedMessages);
socket.send(serverToClientMessage.serializeBinary().buffer, true);
socket.batchedMessages = new BatchMessage();
socket.batchTimeout = null;
}, 100);
}
2020-04-29 01:40:32 +02:00
}
2020-05-03 16:28:18 +02:00
export class IoSocketController {
private Worlds: Map<string, World> = new Map<string, World>();
private sockets: Map<number, ExSocketInterface> = new Map<number, ExSocketInterface>();
private nbClientsGauge: Gauge<string>;
private nbClientsPerRoomGauge: Gauge<string>;
private nextUserId: number = 1;
2020-05-03 16:28:18 +02:00
2020-09-28 18:52:54 +02:00
constructor(private readonly app: TemplatedApp) {
2020-05-03 16:28:18 +02:00
this.nbClientsGauge = new Gauge({
name: 'workadventure_nb_sockets',
help: 'Number of connected sockets',
labelNames: [ ]
});
this.nbClientsPerRoomGauge = new Gauge({
name: 'workadventure_nb_clients_per_room',
help: 'Number of clients per room',
labelNames: [ 'room' ]
});
this.ioConnection();
}
private isValidToken(token: object): token is TokenInterface {
if (typeof((token as TokenInterface).userUuid) !== 'string') {
return false;
}
return true;
}
private async authenticate(req: HttpRequest): Promise<{ token: string, userUuid: string }> {
2020-09-28 18:52:54 +02:00
//console.log(socket.handshake.query.token);
const query = parse(req.getQuery());
2020-05-10 19:54:41 +02:00
if (!query.token) {
throw new Error('An authentication error happened, a user tried to connect without a token.');
}
const token = query.token;
if (typeof(token) !== "string") {
throw new Error('Token is expected to be a string');
}
2020-09-30 12:16:39 +02:00
if(token === 'test') {
if (ALLOW_ARTILLERY) {
return {
token,
2020-10-01 16:51:51 +02:00
userUuid: uuidv4()
}
} else {
throw new Error("In order to perform a load-testing test on this environment, you must set the ALLOW_ARTILLERY environment variable to 'true'");
2020-09-09 12:32:01 +02:00
}
}
2020-09-16 16:06:43 +02:00
/*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'));
}*/
2020-09-16 16:06:43 +02:00
const promise = new Promise<{ token: string, userUuid: string }>((resolve, reject) => {
Jwt.verify(token, SECRET_KEY, {},(err, tokenDecoded) => {
if (err) {
2020-06-10 12:32:39 +02:00
console.error('An authentication error happened, invalid JsonWebToken.', err);
reject(new Error('An authentication error happened, invalid JsonWebToken. '+err.message));
return;
}
if (tokenDecoded === undefined) {
console.error('Empty token found.');
reject(new Error('Empty token found.'));
return;
}
const tokenInterface = tokenDecoded as TokenInterface;
2020-09-16 16:06:43 +02:00
2020-09-28 15:02:37 +02:00
if (!this.isValidToken(tokenInterface)) {
reject(new Error('Authentication error, invalid token structure.'));
return;
}
2020-07-27 22:36:07 +02:00
resolve({
token,
userUuid: tokenInterface.userUuid
});
});
});
2020-07-27 22:36:07 +02:00
return promise;
2020-09-28 18:52:54 +02:00
}
2020-09-16 11:41:03 +02:00
2020-09-28 18:52:54 +02:00
ioConnection() {
this.app.ws('/*', {
2020-09-28 18:52:54 +02:00
/* Options */
//compression: uWS.SHARED_COMPRESSOR,
maxPayloadLength: 16 * 1024 * 1024,
2020-09-30 12:16:39 +02:00
maxBackpressure: 65536, // Maximum 64kB of data in the buffer.
2020-09-29 16:01:22 +02:00
//idleTimeout: 10,
upgrade: (res, req, context) => {
2020-09-30 12:16:39 +02:00
//console.log('An Http connection wants to become WebSocket, URL: ' + req.getUrl() + '!');
(async () => {
/* Keep track of abortions */
const upgradeAborted = {aborted: false};
res.onAborted(() => {
/* We can simply signal that we were aborted */
upgradeAborted.aborted = true;
2020-07-27 22:36:07 +02:00
});
try {
const result = await this.authenticate(req);
if (upgradeAborted.aborted) {
console.log("Ouch! Client disconnected before we could upgrade it!");
/* You must not upgrade now */
return;
}
/* This immediately calls open handler, you must not use res after this call */
res.upgrade({
// Data passed here is accessible on the "websocket" socket object.
url: req.getUrl(),
token: result.token,
userUuid: result.userUuid
},
/* Spell these correctly */
req.getHeader('sec-websocket-key'),
req.getHeader('sec-websocket-protocol'),
req.getHeader('sec-websocket-extensions'),
context);
2020-09-30 10:17:01 +02:00
} catch (e) {
if (e instanceof Error) {
console.warn(e.message);
res.writeStatus("401 Unauthorized").end(e.message);
} else {
console.warn(e);
res.writeStatus("500 Internal Server Error").end('An error occurred');
}
return;
}
})();
},
2020-09-28 18:52:54 +02:00
/* Handlers */
open: (ws) => {
const client : ExSocketInterface = ws as ExSocketInterface;
client.userId = this.nextUserId;
this.nextUserId++;
client.userUuid = ws.userUuid;
client.token = ws.token;
2020-09-28 18:52:54 +02:00
client.batchedMessages = new BatchMessage();
client.batchTimeout = null;
client.emitInBatch = (payload: SubMessage): void => {
emitInBatch(client, payload);
}
2020-09-28 18:52:54 +02:00
client.disconnecting = false;
this.sockets.set(client.userId, client);
// Let's log server load when a user joins
this.nbClientsGauge.inc();
console.log(new Date().toISOString() + ' A user joined (', this.sockets.size, ' connected users)');
},
message: (ws, arrayBuffer, isBinary) => {
const client = ws as ExSocketInterface;
const message = ClientToServerMessage.deserializeBinary(new Uint8Array(arrayBuffer));
if (message.hasJoinroommessage()) {
this.handleJoinRoom(client, message.getJoinroommessage() as JoinRoomMessage);
} else if (message.hasViewportmessage()) {
this.handleViewport(client, message.getViewportmessage() as ViewportMessage);
} else if (message.hasUsermovesmessage()) {
this.handleUserMovesMessage(client, message.getUsermovesmessage() as UserMovesMessage);
} else if (message.hasSetplayerdetailsmessage()) {
this.handleSetPlayerDetails(client, message.getSetplayerdetailsmessage() as SetPlayerDetailsMessage);
} else if (message.hasSilentmessage()) {
this.handleSilentMessage(client, message.getSilentmessage() as SilentMessage);
} else if (message.hasItemeventmessage()) {
this.handleItemEvent(client, message.getItemeventmessage() as ItemEventMessage);
2020-09-29 16:01:22 +02:00
} else if (message.hasWebrtcsignaltoservermessage()) {
2020-10-12 11:22:41 +02:00
this.emitVideo(client, message.getWebrtcsignaltoservermessage() as WebRtcSignalToServerMessage);
2020-09-29 16:01:22 +02:00
} else if (message.hasWebrtcscreensharingsignaltoservermessage()) {
2020-10-12 11:22:41 +02:00
this.emitScreenSharing(client, message.getWebrtcscreensharingsignaltoservermessage() as WebRtcSignalToServerMessage);
} else if (message.hasPlayglobalmessage()) {
2020-10-12 11:22:41 +02:00
this.emitPlayGlobalMessage(client, message.getPlayglobalmessage() as PlayGlobalMessage);
} else if (message.hasReportplayermessage()){
this.handleReportMessage(client, message.getReportplayermessage() as ReportPlayerMessage);
}
2020-06-11 23:18:06 +02:00
2020-09-28 18:52:54 +02:00
/* Ok is false if backpressure was built up, wait for drain */
//let ok = ws.send(message, isBinary);
},
drain: (ws) => {
console.log('WebSocket backpressure: ' + ws.getBufferedAmount());
},
close: (ws, code, message) => {
const Client = (ws as ExSocketInterface);
try {
2020-09-28 18:52:54 +02:00
Client.disconnecting = true;
//leave room
this.leaveRoom(Client);
//delete all socket information
2020-09-29 17:24:16 +02:00
/*delete Client.roomId;
delete Client.token;
2020-09-29 17:24:16 +02:00
delete Client.position;*/
} catch (e) {
console.error('An error occurred on "disconnect"');
console.error(e);
}
2020-09-28 18:52:54 +02:00
this.sockets.delete(Client.userId);
// Let's log server load when a user leaves
this.nbClientsGauge.dec();
2020-09-28 18:52:54 +02:00
console.log('A user left (', this.sockets.size, ' connected users)');
}
})
// TODO: finish this!
/*this.Io.on(SocketIoEvent.CONNECTION, (socket: Socket) => {
socket.on(SocketIoEvent.WEBRTC_SIGNAL, (data: unknown) => {
this.emitVideo((socket as ExSocketInterface), data);
2020-04-29 01:40:32 +02:00
});
2020-09-28 18:52:54 +02:00
socket.on(SocketIoEvent.WEBRTC_SCREEN_SHARING_SIGNAL, (data: unknown) => {
this.emitScreenSharing((socket as ExSocketInterface), data);
});
2020-09-28 18:52:54 +02:00
});*/
}
2020-09-28 18:52:54 +02:00
private emitError(Client: ExSocketInterface, message: string): void {
const errorMessage = new ErrorMessage();
errorMessage.setMessage(message);
2020-09-28 18:52:54 +02:00
const serverToClientMessage = new ServerToClientMessage();
serverToClientMessage.setErrormessage(errorMessage);
2020-07-27 22:36:07 +02:00
2020-09-28 18:52:54 +02:00
if (!Client.disconnecting) {
2020-09-29 16:01:22 +02:00
Client.send(serverToClientMessage.serializeBinary().buffer, true);
2020-09-28 18:52:54 +02:00
}
console.warn(message);
}
2020-09-24 17:24:37 +02:00
2020-09-28 18:52:54 +02:00
private handleJoinRoom(Client: ExSocketInterface, message: JoinRoomMessage): void {
try {
/*if (!isJoinRoomMessageInterface(message.toObject())) {
console.log(message.toObject())
this.emitError(Client, 'Invalid JOIN_ROOM message received: ' + message.toObject().toString());
return;
}*/
const roomId = message.getRoomid();
if (Client.roomId === roomId) {
return;
}
2020-07-27 22:36:07 +02:00
2020-09-28 18:52:54 +02:00
//leave previous room
2020-09-29 17:24:16 +02:00
//this.leaveRoom(Client); // Useless now, there is only one room per connection
2020-07-27 22:36:07 +02:00
2020-09-28 18:52:54 +02:00
//join new previous room
const world = this.joinRoom(Client, roomId, ProtobufUtils.toPointInterface(message.getPosition() as PositionMessage));
2020-07-27 22:36:07 +02:00
2020-09-28 18:52:54 +02:00
const things = world.setViewport(Client, (message.getViewport() as ViewportMessage).toObject());
2020-09-24 17:24:37 +02:00
2020-09-28 18:52:54 +02:00
const roomJoinedMessage = new RoomJoinedMessage();
2020-09-24 17:24:37 +02:00
2020-09-28 18:52:54 +02:00
for (const thing of things) {
if (thing instanceof User) {
const player: ExSocketInterface|undefined = this.sockets.get(thing.id);
if (player === undefined) {
console.warn('Something went wrong. The World contains a user "'+thing.id+"' but this user does not exist in the sockets list!");
continue;
2020-07-27 22:36:07 +02:00
}
2020-09-24 17:24:37 +02:00
2020-09-28 18:52:54 +02:00
const userJoinedMessage = new UserJoinedMessage();
userJoinedMessage.setUserid(thing.id);
userJoinedMessage.setName(player.name);
userJoinedMessage.setCharacterlayersList(player.characterLayers);
userJoinedMessage.setPosition(ProtobufUtils.toPositionMessage(player.position));
roomJoinedMessage.addUser(userJoinedMessage);
} else if (thing instanceof Group) {
const groupUpdateMessage = new GroupUpdateMessage();
groupUpdateMessage.setGroupid(thing.getId());
groupUpdateMessage.setPosition(ProtobufUtils.toPointMessage(thing.getPosition()));
roomJoinedMessage.addGroup(groupUpdateMessage);
} else {
console.error("Unexpected type for Movable returned by setViewport");
2020-07-27 22:36:07 +02:00
}
2020-09-28 18:52:54 +02:00
}
2020-09-28 18:52:54 +02:00
for (const [itemId, item] of world.getItemsState().entries()) {
const itemStateMessage = new ItemStateMessage();
itemStateMessage.setItemid(itemId);
itemStateMessage.setStatejson(JSON.stringify(item));
roomJoinedMessage.addItem(itemStateMessage);
}
const serverToClientMessage = new ServerToClientMessage();
serverToClientMessage.setRoomjoinedmessage(roomJoinedMessage);
if (!Client.disconnecting) {
Client.send(serverToClientMessage.serializeBinary().buffer, true);
}
} catch (e) {
console.error('An error occurred on "join_room" event');
console.error(e);
}
}
private handleViewport(client: ExSocketInterface, viewportMessage: ViewportMessage) {
try {
const viewport = viewportMessage.toObject();
client.viewport = viewport;
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);
}
}
private handleUserMovesMessage(client: ExSocketInterface, userMovesMessage: UserMovesMessage) {
//console.log(SockerIoEvent.USER_POSITION, userMovesMessage);
try {
const userMoves = userMovesMessage.toObject();
// If CPU is high, let's drop messages of users moving (we will only dispatch the final position)
2020-09-30 14:42:35 +02:00
if (cpuTracker.isOverHeating() && userMoves.position?.moving === true) {
return;
}
2020-09-28 18:52:54 +02:00
const position = userMoves.position;
if (position === undefined) {
throw new Error('Position not found in message');
}
const viewport = userMoves.viewport;
if (viewport === undefined) {
throw new Error('Viewport not found in message');
}
let direction: string;
switch (position.direction) {
case Direction.UP:
direction = 'up';
break;
case Direction.DOWN:
direction = 'down';
break;
case Direction.LEFT:
direction = 'left';
break;
case Direction.RIGHT:
direction = 'right';
break;
default:
throw new Error("Unexpected direction");
}
// sending to all clients in room except sender
client.position = {
x: position.x,
y: position.y,
direction,
moving: position.moving,
};
client.viewport = viewport;
// update position in the world
const world = this.Worlds.get(client.roomId);
if (!world) {
console.error("In USER_POSITION, could not find world with id '", client.roomId, "'");
return;
}
world.updatePosition(client, client.position);
world.setViewport(client, client.viewport);
} catch (e) {
console.error('An error occurred on "user_position" event');
console.error(e);
}
2020-04-29 01:40:32 +02:00
}
2020-09-28 18:52:54 +02:00
private handleSetPlayerDetails(client: ExSocketInterface, playerDetailsMessage: SetPlayerDetailsMessage) {
const playerDetails = {
name: playerDetailsMessage.getName(),
characterLayers: playerDetailsMessage.getCharacterlayersList()
};
//console.log(SocketIoEvent.SET_PLAYER_DETAILS, playerDetails);
if (!isSetPlayerDetailsMessage(playerDetails)) {
this.emitError(client, 'Invalid SET_PLAYER_DETAILS message received: ');
2020-06-11 23:18:06 +02:00
return;
}
2020-09-28 18:52:54 +02:00
client.name = playerDetails.name;
client.characterLayers = playerDetails.characterLayers;
const setUserIdMessage = new SetUserIdMessage();
setUserIdMessage.setUserid(client.userId);
const serverToClientMessage = new ServerToClientMessage();
serverToClientMessage.setSetuseridmessage(setUserIdMessage);
if (!client.disconnecting) {
client.send(serverToClientMessage.serializeBinary().buffer, true);
}
}
private handleSilentMessage(client: ExSocketInterface, silentMessage: SilentMessage) {
try {
// update position in the world
const world = this.Worlds.get(client.roomId);
if (!world) {
console.error("In handleSilentMessage, could not find world with id '", client.roomId, "'");
return;
}
world.setSilent(client, silentMessage.getSilent());
} catch (e) {
console.error('An error occurred on "handleSilentMessage"');
console.error(e);
}
}
private handleItemEvent(ws: ExSocketInterface, itemEventMessage: ItemEventMessage) {
const itemEvent = ProtobufUtils.toItemEvent(itemEventMessage);
try {
const world = this.Worlds.get(ws.roomId);
if (!world) {
console.error("Could not find world with id '", ws.roomId, "'");
return;
}
const subMessage = new SubMessage();
subMessage.setItemeventmessage(itemEventMessage);
// Let's send the event without using the SocketIO room.
for (const user of world.getUsers().values()) {
const client = this.searchClientByIdOrFail(user.id);
//client.emit(SocketIoEvent.ITEM_EVENT, itemEvent);
emitInBatch(client, subMessage);
}
world.setItemState(itemEvent.itemId, itemEvent.state);
} catch (e) {
console.error('An error occurred on "item_event"');
console.error(e);
}
2020-04-29 01:40:32 +02:00
}
2020-10-12 11:22:41 +02:00
private handleReportMessage(client: ExSocketInterface, reportPlayerMessage: ReportPlayerMessage) {
try {
const reportedSocket = this.sockets.get(reportPlayerMessage.getReporteduserid());
2020-10-13 09:37:38 +02:00
if (!reportedSocket) {
2020-10-12 11:22:41 +02:00
throw 'reported socket user not found';
}
//TODO report user on admin application
2020-10-13 09:37:38 +02:00
Axios.post(`${ADMIN_API_URL}/aoi/report`, {
reportedUserUuid: client.userUuid,
reportedUserComment: reportPlayerMessage.getReportcomment(),
reporterUserUuid: client.userUuid,
},
{
headers: {"Authorization": `${ADMIN_API_TOKEN}`}
}).catch((err) => {
throw err;
2020-10-12 11:22:41 +02:00
});
} catch (e) {
console.error('An error occurred on "handleReportMessage"');
console.error(e);
}
}
2020-09-29 16:01:22 +02:00
emitVideo(socket: ExSocketInterface, data: WebRtcSignalToServerMessage): void {
2020-06-11 23:18:06 +02:00
//send only at user
2020-09-29 16:01:22 +02:00
const client = this.sockets.get(data.getReceiverid());
2020-06-11 23:18:06 +02:00
if (client === undefined) {
2020-09-29 16:01:22 +02:00
console.warn("While exchanging a WebRTC signal: client with id ", data.getReceiverid(), " does not exist. This might be a race condition.");
2020-06-11 23:18:06 +02:00
return;
}
2020-09-29 16:01:22 +02:00
const webrtcSignalToClient = new WebRtcSignalToClientMessage();
webrtcSignalToClient.setUserid(socket.userId);
webrtcSignalToClient.setSignal(data.getSignal());
const serverToClientMessage = new ServerToClientMessage();
serverToClientMessage.setWebrtcsignaltoclientmessage(webrtcSignalToClient);
if (!client.disconnecting) {
client.send(serverToClientMessage.serializeBinary().buffer, true);
}
2020-09-29 16:01:22 +02:00
}
emitScreenSharing(socket: ExSocketInterface, data: WebRtcSignalToServerMessage): void {
//send only at user
2020-09-29 16:01:22 +02:00
const client = this.sockets.get(data.getReceiverid());
if (client === undefined) {
2020-09-29 16:01:22 +02:00
console.warn("While exchanging a WEBRTC_SCREEN_SHARING signal: client with id ", data.getReceiverid(), " does not exist. This might be a race condition.");
return;
}
2020-09-29 16:01:22 +02:00
const webrtcSignalToClient = new WebRtcSignalToClientMessage();
webrtcSignalToClient.setUserid(socket.userId);
webrtcSignalToClient.setSignal(data.getSignal());
const serverToClientMessage = new ServerToClientMessage();
serverToClientMessage.setWebrtcscreensharingsignaltoclientmessage(webrtcSignalToClient);
if (!client.disconnecting) {
client.send(serverToClientMessage.serializeBinary().buffer, true);
}
}
searchClientByIdOrFail(userId: number): ExSocketInterface {
const client: ExSocketInterface|undefined = this.sockets.get(userId);
if (client === undefined) {
throw new Error("Could not find user with id " + userId);
2020-05-08 21:17:52 +02:00
}
return client;
2020-05-03 16:28:18 +02:00
}
2020-05-10 19:54:41 +02:00
leaveRoom(Client : ExSocketInterface){
// leave previous room and world
2020-05-10 19:54:41 +02:00
if(Client.roomId){
try {
//user leave previous world
const world: World | undefined = this.Worlds.get(Client.roomId);
if (world) {
world.leave(Client);
if (world.isEmpty()) {
this.Worlds.delete(Client.roomId);
}
2020-06-29 19:14:54 +02:00
}
//user leave previous room
2020-09-28 18:52:54 +02:00
//Client.leave(Client.roomId);
} finally {
this.nbClientsPerRoomGauge.dec({ room: Client.roomId });
2020-09-29 17:24:16 +02:00
//delete Client.roomId;
2020-05-10 19:54:41 +02:00
}
}
}
2020-05-15 23:24:04 +02:00
private joinRoom(Client : ExSocketInterface, roomId: string, position: PointInterface): World {
2020-05-10 19:54:41 +02:00
//join user in room
2020-09-28 18:52:54 +02:00
//Client.join(roomId);
this.nbClientsPerRoomGauge.inc({ room: roomId });
2020-05-15 23:24:04 +02:00
Client.roomId = roomId;
Client.position = position;
2020-05-10 19:54:41 +02:00
//check and create new world for a room
let world = this.Worlds.get(roomId)
if(world === undefined){
2020-09-29 16:01:22 +02:00
world = new World((user1: User, group: Group) => {
this.joinWebRtcRoom(user1, group);
}, (user1: User, group: Group) => {
2020-05-10 19:54:41 +02:00
this.disConnectedUser(user1, group);
}, MINIMUM_DISTANCE, GROUP_RADIUS, (thing: Movable, listener: User) => {
const clientListener = this.searchClientByIdOrFail(listener.id);
2020-09-16 16:06:43 +02:00
if (thing instanceof User) {
const clientUser = this.searchClientByIdOrFail(thing.id);
2020-09-24 14:50:28 +02:00
const userJoinedMessage = new UserJoinedMessage();
2020-09-28 18:52:54 +02:00
if (!Number.isInteger(clientUser.userId)) {
throw new Error('clientUser.userId is not an integer '+clientUser.userId);
}
2020-09-24 14:50:28 +02:00
userJoinedMessage.setUserid(clientUser.userId);
userJoinedMessage.setName(clientUser.name);
userJoinedMessage.setCharacterlayersList(clientUser.characterLayers);
userJoinedMessage.setPosition(ProtobufUtils.toPositionMessage(clientUser.position));
const subMessage = new SubMessage();
subMessage.setUserjoinedmessage(userJoinedMessage);
2020-09-28 18:52:54 +02:00
emitInBatch(clientListener, subMessage);
2020-09-16 16:06:43 +02:00
} else if (thing instanceof Group) {
2020-09-21 11:24:03 +02:00
this.emitCreateUpdateGroupEvent(clientListener, thing);
2020-09-16 16:06:43 +02:00
} else {
console.error('Unexpected type for Movable.');
}
}, (thing: Movable, position, listener) => {
const clientListener = this.searchClientByIdOrFail(listener.id);
2020-09-16 16:06:43 +02:00
if (thing instanceof User) {
const clientUser = this.searchClientByIdOrFail(thing.id);
const userMovedMessage = new UserMovedMessage();
userMovedMessage.setUserid(clientUser.userId);
2020-09-18 18:16:26 +02:00
userMovedMessage.setPosition(ProtobufUtils.toPositionMessage(clientUser.position));
const subMessage = new SubMessage();
subMessage.setUsermovedmessage(userMovedMessage);
2020-09-28 18:52:54 +02:00
clientListener.emitInBatch(subMessage);
2020-09-16 16:06:43 +02:00
//console.log("Sending USER_MOVED event");
} else if (thing instanceof Group) {
2020-09-21 11:24:03 +02:00
this.emitCreateUpdateGroupEvent(clientListener, thing);
2020-09-16 16:06:43 +02:00
} else {
console.error('Unexpected type for Movable.');
}
}, (thing: Movable, listener) => {
const clientListener = this.searchClientByIdOrFail(listener.id);
2020-09-16 16:06:43 +02:00
if (thing instanceof User) {
const clientUser = this.searchClientByIdOrFail(thing.id);
2020-09-24 16:11:47 +02:00
this.emitUserLeftEvent(clientListener, clientUser.userId);
2020-09-16 16:06:43 +02:00
} else if (thing instanceof Group) {
2020-09-24 10:05:16 +02:00
this.emitDeleteGroupEvent(clientListener, thing.getId());
2020-09-16 16:06:43 +02:00
} else {
console.error('Unexpected type for Movable.');
}
2020-05-10 19:54:41 +02:00
});
2020-05-15 23:24:04 +02:00
this.Worlds.set(roomId, world);
2020-05-10 19:54:41 +02:00
}
// Dispatch groups position to newly connected user
world.getGroups().forEach((group: Group) => {
2020-09-24 10:05:16 +02:00
this.emitCreateUpdateGroupEvent(Client, group);
});
//join world
world.join(Client, Client.position);
return world;
2020-05-10 19:54:41 +02:00
}
2020-09-28 18:52:54 +02:00
private emitCreateUpdateGroupEvent(client: ExSocketInterface, group: Group): void {
2020-09-21 11:24:03 +02:00
const position = group.getPosition();
const pointMessage = new PointMessage();
pointMessage.setX(Math.floor(position.x));
pointMessage.setY(Math.floor(position.y));
const groupUpdateMessage = new GroupUpdateMessage();
groupUpdateMessage.setGroupid(group.getId());
groupUpdateMessage.setPosition(pointMessage);
2020-09-24 10:05:16 +02:00
const subMessage = new SubMessage();
subMessage.setGroupupdatemessage(groupUpdateMessage);
2020-09-28 18:52:54 +02:00
emitInBatch(client, subMessage);
2020-09-24 10:05:16 +02:00
//socket.emit(SocketIoEvent.GROUP_CREATE_UPDATE, groupUpdateMessage.serializeBinary().buffer);
}
2020-09-29 10:57:14 +02:00
private emitDeleteGroupEvent(client: ExSocketInterface, groupId: number): void {
2020-09-24 10:05:16 +02:00
const groupDeleteMessage = new GroupDeleteMessage();
groupDeleteMessage.setGroupid(groupId);
const subMessage = new SubMessage();
subMessage.setGroupdeletemessage(groupDeleteMessage);
2020-09-28 18:52:54 +02:00
emitInBatch(client, subMessage);
2020-09-21 11:24:03 +02:00
}
2020-09-29 10:57:14 +02:00
private emitUserLeftEvent(client: ExSocketInterface, userId: number): void {
2020-09-24 16:11:47 +02:00
const userLeftMessage = new UserLeftMessage();
userLeftMessage.setUserid(userId);
const subMessage = new SubMessage();
subMessage.setUserleftmessage(userLeftMessage);
2020-09-28 18:52:54 +02:00
emitInBatch(client, subMessage);
2020-09-24 16:11:47 +02:00
}
2020-09-29 16:01:22 +02:00
joinWebRtcRoom(user: User, group: Group) {
/*const roomId: string = "webrtcroom"+group.getId();
if (user.socket.webRtcRoomId === roomId) {
2020-04-29 01:40:32 +02:00
return;
2020-09-29 16:01:22 +02:00
}*/
2020-09-28 18:52:54 +02:00
2020-09-29 16:01:22 +02:00
for (const otherUser of group.getUsers()) {
if (user === otherUser) {
continue;
}
// Let's send 2 messages: one to the user joining the group and one to the other user
const webrtcStartMessage1 = new WebRtcStartMessage();
webrtcStartMessage1.setUserid(otherUser.id);
webrtcStartMessage1.setName(otherUser.socket.name);
webrtcStartMessage1.setInitiator(true);
const serverToClientMessage1 = new ServerToClientMessage();
serverToClientMessage1.setWebrtcstartmessage(webrtcStartMessage1);
if (!user.socket.disconnecting) {
user.socket.send(serverToClientMessage1.serializeBinary().buffer, true);
2020-09-29 17:24:16 +02:00
//console.log('Sending webrtcstart initiator to '+user.socket.userId)
2020-09-29 16:01:22 +02:00
}
const webrtcStartMessage2 = new WebRtcStartMessage();
webrtcStartMessage2.setUserid(user.id);
webrtcStartMessage2.setName(user.socket.name);
webrtcStartMessage2.setInitiator(false);
const serverToClientMessage2 = new ServerToClientMessage();
serverToClientMessage2.setWebrtcstartmessage(webrtcStartMessage2);
if (!otherUser.socket.disconnecting) {
otherUser.socket.send(serverToClientMessage2.serializeBinary().buffer, true);
2020-09-29 17:24:16 +02:00
//console.log('Sending webrtcstart to '+otherUser.socket.userId)
2020-09-29 16:01:22 +02:00
}
2020-09-28 18:52:54 +02:00
2020-04-29 01:40:32 +02:00
}
2020-09-29 16:01:22 +02:00
/* socket.join(roomId);
2020-04-29 01:40:32 +02:00
socket.webRtcRoomId = roomId;
//if two persons in room share
2020-09-29 09:45:47 +02:00
if (this.Io.sockets.adapter.rooms[roomId].length < 2) {
2020-04-29 01:40:32 +02:00
return;
}
// TODO: scanning all sockets is maybe not the most efficient
const clients: Array<ExSocketInterface> = (Object.values(this.Io.sockets.sockets) as Array<ExSocketInterface>)
.filter((client: ExSocketInterface) => client.webRtcRoomId && client.webRtcRoomId === roomId);
2020-04-29 01:40:32 +02:00
//send start at one client to initialise offer webrtc
//send all users in room to create PeerConnection in front
clients.forEach((client: ExSocketInterface, index: number) => {
const peerClients = clients.reduce((tabs: Array<UserInGroupInterface>, clientId: ExSocketInterface, indexClientId: number) => {
if (!clientId.userId || clientId.userId === client.userId) {
2020-04-29 01:40:32 +02:00
return tabs;
}
tabs.push({
userId: clientId.userId,
name: clientId.name,
2020-04-29 01:40:32 +02:00
initiator: index <= indexClientId
});
return tabs;
}, []);
client.emit(SocketIoEvent.WEBRTC_START, {clients: peerClients, roomId: roomId});
2020-09-29 09:45:47 +02:00
});*/
}
/** permit to share user position
2020-05-03 16:28:18 +02:00
** users position will send in event 'user-position'
** The data sent is an array with information for each user :
[
{
userId: <string>,
roomId: <string>,
position: {
x : <number>,
y : <number>,
direction: <string>
}
},
2020-05-03 16:28:18 +02:00
...
]
**/
2020-04-27 00:44:25 +02:00
//disconnect user
2020-09-29 16:01:22 +02:00
disConnectedUser(user: User, group: Group) {
// Most of the time, sending a disconnect event to one of the players is enough (the player will close the connection
// which will be shut for the other player).
// However! In the rare case where the WebRTC connection is not yet established, if we close the connection on one of the player,
// the other player will try connecting until a timeout happens (during this time, the connection icon will be displayed for nothing).
// So we also send the disconnect event to the other player.
2020-09-29 16:01:22 +02:00
for (const otherUser of group.getUsers()) {
if (user === otherUser) {
continue;
}
const webrtcDisconnectMessage1 = new WebRtcDisconnectMessage();
webrtcDisconnectMessage1.setUserid(user.id);
const serverToClientMessage1 = new ServerToClientMessage();
serverToClientMessage1.setWebrtcdisconnectmessage(webrtcDisconnectMessage1);
if (!otherUser.socket.disconnecting) {
otherUser.socket.send(serverToClientMessage1.serializeBinary().buffer, true);
}
const webrtcDisconnectMessage2 = new WebRtcDisconnectMessage();
webrtcDisconnectMessage2.setUserid(otherUser.id);
const serverToClientMessage2 = new ServerToClientMessage();
serverToClientMessage2.setWebrtcdisconnectmessage(webrtcDisconnectMessage2);
if (!user.socket.disconnecting) {
user.socket.send(serverToClientMessage2.serializeBinary().buffer, true);
}
}
//disconnect webrtc room
2020-09-29 16:01:22 +02:00
/*if(!Client.webRtcRoomId){
2020-05-03 16:28:18 +02:00
return;
2020-09-29 16:01:22 +02:00
}*/
//Client.leave(Client.webRtcRoomId);
//delete Client.webRtcRoomId;
2020-04-27 00:44:25 +02:00
}
private emitPlayGlobalMessage(client: ExSocketInterface, playglobalmessage: PlayGlobalMessage) {
try {
const world = this.Worlds.get(client.roomId);
if (!world) {
console.error("In emitPlayGlobalMessage, could not find world with id '", client.roomId, "'");
return;
}
const serverToClientMessage = new ServerToClientMessage();
serverToClientMessage.setPlayglobalmessage(playglobalmessage);
for (const [id, user] of world.getUsers().entries()) {
user.socket.send(serverToClientMessage.serializeBinary().buffer, true);
}
} catch (e) {
console.error('An error occurred on "emitPlayGlobalMessage" event');
console.error(e);
2020-05-03 16:28:18 +02:00
}
}
public getWorlds(): Map<string, World> {
return this.Worlds;
2020-04-27 00:44:25 +02:00
}
/**
*
* @param token
*/
searchClientByUuid(uuid: string): ExSocketInterface | null {
for(const socket of this.sockets.values()){
if(socket.userUuid === uuid){
return socket;
}
}
return null;
}
public teleport(userUuid: string) {
const user = this.searchClientByUuid(userUuid);
if(!user){
throw 'User not found';
}
const teleportMessageMessage = new TeleportMessageMessage();
teleportMessageMessage.setMap(`/teleport/${user.userUuid}`);
user.send(teleportMessageMessage.serializeBinary().buffer, true);
}
2020-04-04 22:35:20 +02:00
}