Merge pull request #290 from thecodingmachine/authorisationMap

[WIP] Adding support for authorization in maps
This commit is contained in:
Kharhamel 2020-10-09 17:30:38 +02:00 committed by GitHub
commit ad06650920
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 476 additions and 519 deletions

View File

@ -1,22 +1,14 @@
import Jwt from "jsonwebtoken"; import {URL_ROOM_STARTED} from "../Enum/EnvironmentVariable"; //TODO fix import by "_Enum/..."
import {ADMIN_API_TOKEN, ADMIN_API_URL, SECRET_KEY, URL_ROOM_STARTED} from "../Enum/EnvironmentVariable"; //TODO fix import by "_Enum/..."
import { v4 } from 'uuid'; import { v4 } from 'uuid';
import {HttpRequest, HttpResponse, TemplatedApp} from "uWebSockets.js"; import {HttpRequest, HttpResponse, TemplatedApp} from "uWebSockets.js";
import {BaseController} from "./BaseController"; import {BaseController} from "./BaseController";
import Axios from "axios"; import {adminApi, AdminApiData} from "../Services/AdminApi";
import {jwtTokenManager} from "../Services/JWTTokenManager";
export interface TokenInterface { export interface TokenInterface {
userUuid: string userUuid: string
} }
interface AdminApiData {
organizationSlug: string
worldSlug: string
roomSlug: string
mapUrlStart: string
userUuid: string
}
export class AuthenticateController extends BaseController { export class AuthenticateController extends BaseController {
constructor(private App : TemplatedApp) { constructor(private App : TemplatedApp) {
@ -44,6 +36,7 @@ export class AuthenticateController extends BaseController {
//todo: what to do if the organizationMemberToken is already used? //todo: what to do if the organizationMemberToken is already used?
const organizationMemberToken:string|null = param.organizationMemberToken; const organizationMemberToken:string|null = param.organizationMemberToken;
const mapSlug:string|null = param.mapSlug;
try { try {
let userUuid; let userUuid;
@ -51,24 +44,22 @@ export class AuthenticateController extends BaseController {
let newUrl: string|null = null; let newUrl: string|null = null;
if (organizationMemberToken) { if (organizationMemberToken) {
if (!ADMIN_API_URL) { const data = await adminApi.fetchMemberDataByToken(organizationMemberToken);
return res.status(401).send('No admin backoffice set!');
}
//todo: this call can fail if the corresponding world is not activated or if the token is invalid. Handle that case.
const data = await Axios.get(ADMIN_API_URL+'/api/login-url/'+organizationMemberToken,
{ headers: {"Authorization" : `${ADMIN_API_TOKEN}`} }
).then((res): AdminApiData => res.data);
userUuid = data.userUuid; userUuid = data.userUuid;
mapUrlStart = data.mapUrlStart; mapUrlStart = data.mapUrlStart;
newUrl = this.getNewUrlOnAdminAuth(data) newUrl = this.getNewUrlOnAdminAuth(data)
} else if (mapSlug !== null) {
userUuid = v4();
mapUrlStart = mapSlug;
newUrl = null;
} else { } else {
userUuid = v4(); userUuid = v4();
mapUrlStart = host.replace('api.', 'maps.') + URL_ROOM_STARTED; mapUrlStart = host.replace('api.', 'maps.') + URL_ROOM_STARTED;
newUrl = null; newUrl = '_/global/'+mapUrlStart;
} }
const authToken = Jwt.sign({userUuid: userUuid}, SECRET_KEY, {expiresIn: '24h'}); const authToken = jwtTokenManager.createJWTToken(userUuid);
res.writeStatus("200 OK").end(JSON.stringify({ res.writeStatus("200 OK").end(JSON.stringify({
authToken, authToken,
userUuid, userUuid,

View File

@ -1,23 +1,11 @@
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 {ExSocketInterface} from "../Model/Websocket/ExSocketInterface"; //TODO fix import by "_Model/.."
import Jwt, {JsonWebTokenError} from "jsonwebtoken"; import {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 {GameRoom} from "../Model/GameRoom";
import {World} from "../Model/World";
import {Group} from "../Model/Group"; import {Group} from "../Model/Group";
import {User} from "../Model/User"; import {User} from "../Model/User";
import {isSetPlayerDetailsMessage,} from "../Model/Websocket/SetPlayerDetailsMessage"; import {isSetPlayerDetailsMessage,} from "../Model/Websocket/SetPlayerDetailsMessage";
import {MessageUserJoined} from "../Model/Websocket/MessageUserJoined";
import si from "systeminformation";
import {Gauge} from "prom-client"; import {Gauge} from "prom-client";
import {TokenInterface} from "../Controller/AuthenticateController";
import {isJoinRoomMessageInterface} from "../Model/Websocket/JoinRoomMessage";
import {PointInterface} from "../Model/Websocket/PointInterface"; import {PointInterface} from "../Model/Websocket/PointInterface";
import {isWebRtcSignalMessageInterface} from "../Model/Websocket/WebRtcSignalMessage";
import {UserInGroupInterface} from "../Model/Websocket/UserInGroupInterface";
import {isItemEventMessageInterface} from "../Model/Websocket/ItemEventMessage";
import { v4 as uuidv4 } from 'uuid';
import {GroupUpdateInterface} from "_Model/Websocket/GroupUpdateInterface";
import {Movable} from "../Model/Movable"; import {Movable} from "../Model/Movable";
import { import {
PositionMessage, PositionMessage,
@ -33,7 +21,6 @@ import {
ItemEventMessage, ItemEventMessage,
ViewportMessage, ViewportMessage,
ClientToServerMessage, ClientToServerMessage,
JoinRoomMessage,
ErrorMessage, ErrorMessage,
RoomJoinedMessage, RoomJoinedMessage,
ItemStateMessage, ItemStateMessage,
@ -42,14 +29,19 @@ import {
SilentMessage, SilentMessage,
WebRtcSignalToClientMessage, WebRtcSignalToClientMessage,
WebRtcSignalToServerMessage, WebRtcSignalToServerMessage,
WebRtcStartMessage, WebRtcDisconnectMessage, PlayGlobalMessage WebRtcStartMessage,
WebRtcDisconnectMessage,
PlayGlobalMessage,
} from "../Messages/generated/messages_pb"; } from "../Messages/generated/messages_pb";
import {UserMovesMessage} from "../Messages/generated/messages_pb"; import {UserMovesMessage} from "../Messages/generated/messages_pb";
import Direction = PositionMessage.Direction; import Direction = PositionMessage.Direction;
import {ProtobufUtils} from "../Model/Websocket/ProtobufUtils"; import {ProtobufUtils} from "../Model/Websocket/ProtobufUtils";
import {App, HttpRequest, TemplatedApp, WebSocket} from "uWebSockets.js" import {TemplatedApp} from "uWebSockets.js"
import {parse} from "query-string"; import {parse} from "query-string";
import {cpuTracker} from "../Services/CpuTracker"; import {cpuTracker} from "../Services/CpuTracker";
import {ViewportInterface} from "../Model/Websocket/ViewportMessage";
import {jwtTokenManager} from "../Services/JWTTokenManager";
import {adminApi} from "../Services/AdminApi";
function emitInBatch(socket: ExSocketInterface, payload: SubMessage): void { function emitInBatch(socket: ExSocketInterface, payload: SubMessage): void {
socket.batchedMessages.addPayload(payload); socket.batchedMessages.addPayload(payload);
@ -71,7 +63,7 @@ function emitInBatch(socket: ExSocketInterface, payload: SubMessage): void {
} }
export class IoSocketController { export class IoSocketController {
private Worlds: Map<string, World> = new Map<string, World>(); private Worlds: Map<string, GameRoom> = new Map<string, GameRoom>();
private sockets: Map<number, ExSocketInterface> = new Map<number, ExSocketInterface>(); private sockets: Map<number, ExSocketInterface> = new Map<number, ExSocketInterface>();
private nbClientsGauge: Gauge<string>; private nbClientsGauge: Gauge<string>;
private nbClientsPerRoomGauge: Gauge<string>; private nbClientsPerRoomGauge: Gauge<string>;
@ -93,92 +85,10 @@ export class IoSocketController {
this.ioConnection(); this.ioConnection();
} }
private isValidToken(token: object): token is TokenInterface {
if (typeof((token as TokenInterface).userUuid) !== 'string') {
return false;
}
return true;
}
/**
*
* @param token
*/
/* searchClientByToken(token: string): ExSocketInterface | null {
const clients: ExSocketInterface[] = Object.values(this.Io.sockets.sockets) as ExSocketInterface[];
for (let i = 0; i < clients.length; i++) {
const client = clients[i];
if (client.token !== token) {
continue
}
return client;
}
return null;
}*/
private async authenticate(req: HttpRequest): Promise<{ token: string, userUuid: string }> {
//console.log(socket.handshake.query.token);
const query = parse(req.getQuery());
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');
}
if(token === 'test') {
if (ALLOW_ARTILLERY) {
return {
token,
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'");
}
}
/*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'));
}*/
const promise = new Promise<{ token: string, userUuid: string }>((resolve, reject) => {
Jwt.verify(token, SECRET_KEY, {},(err, tokenDecoded) => {
if (err) {
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;
if (!this.isValidToken(tokenInterface)) {
reject(new Error('Authentication error, invalid token structure.'));
return;
}
resolve({
token,
userUuid: tokenInterface.userUuid
});
});
});
return promise;
}
ioConnection() { ioConnection() {
this.app.ws('/*', { this.app.ws('/room/*', {
/* Options */ /* Options */
//compression: uWS.SHARED_COMPRESSOR, //compression: uWS.SHARED_COMPRESSOR,
maxPayloadLength: 16 * 1024 * 1024, maxPayloadLength: 16 * 1024 * 1024,
@ -187,7 +97,6 @@ export class IoSocketController {
upgrade: (res, req, context) => { upgrade: (res, req, context) => {
//console.log('An Http connection wants to become WebSocket, URL: ' + req.getUrl() + '!'); //console.log('An Http connection wants to become WebSocket, URL: ' + req.getUrl() + '!');
(async () => { (async () => {
/* Keep track of abortions */ /* Keep track of abortions */
const upgradeAborted = {aborted: false}; const upgradeAborted = {aborted: false};
@ -197,7 +106,47 @@ export class IoSocketController {
}); });
try { try {
const result = await this.authenticate(req); const url = req.getUrl();
const query = parse(req.getQuery());
const websocketKey = req.getHeader('sec-websocket-key');
const websocketProtocol = req.getHeader('sec-websocket-protocol');
const websocketExtensions = req.getHeader('sec-websocket-extensions');
const roomId = req.getUrl().substr(6);
const token = query.token;
const x = Number(query.x);
const y = Number(query.y);
const top = Number(query.top);
const bottom = Number(query.bottom);
const left = Number(query.left);
const right = Number(query.right);
const name = query.name;
if (typeof name !== 'string') {
throw new Error('Expecting name');
}
if (name === '') {
throw new Error('No empty name');
}
let characterLayers = query.characterLayers;
if (characterLayers === null) {
throw new Error('Expecting skin');
}
if (typeof characterLayers === 'string') {
characterLayers = [ characterLayers ];
}
const userUuid = await jwtTokenManager.getUserUuidFromToken(token);
console.log('uuid', userUuid);
const isGranted = await adminApi.memberIsGrantedAccessToRoom(userUuid, roomId);
if (!isGranted) {
console.log('access not granted for user '+userUuid+' and room '+roomId);
throw new Error('Client cannot acces this ressource.')
} else {
console.log('access granted for user '+userUuid+' and room '+roomId);
}
if (upgradeAborted.aborted) { if (upgradeAborted.aborted) {
console.log("Ouch! Client disconnected before we could upgrade it!"); console.log("Ouch! Client disconnected before we could upgrade it!");
@ -208,22 +157,37 @@ export class IoSocketController {
/* This immediately calls open handler, you must not use res after this call */ /* This immediately calls open handler, you must not use res after this call */
res.upgrade({ res.upgrade({
// Data passed here is accessible on the "websocket" socket object. // Data passed here is accessible on the "websocket" socket object.
url: req.getUrl(), url,
token: result.token, token,
userUuid: result.userUuid userUuid,
roomId,
name,
characterLayers,
position: {
x: x,
y: y,
direction: 'down',
moving: false
} as PointInterface,
viewport: {
top,
right,
bottom,
left
}
}, },
/* Spell these correctly */ /* Spell these correctly */
req.getHeader('sec-websocket-key'), websocketKey,
req.getHeader('sec-websocket-protocol'), websocketProtocol,
req.getHeader('sec-websocket-extensions'), websocketExtensions,
context); context);
} catch (e) { } catch (e) {
if (e instanceof Error) { if (e instanceof Error) {
console.warn(e.message); console.log(e.message);
res.writeStatus("401 Unauthorized").end(e.message); res.writeStatus("401 Unauthorized").end(e.message);
} else { } else {
console.warn(e); console.log(e);
res.writeStatus("500 Internal Server Error").end('An error occurred'); res.writeStatus("500 Internal Server Error").end('An error occurred');
} }
return; return;
@ -243,20 +207,35 @@ export class IoSocketController {
emitInBatch(client, payload); emitInBatch(client, payload);
} }
client.disconnecting = false; client.disconnecting = false;
client.name = ws.name;
client.characterLayers = ws.characterLayers;
client.roomId = ws.roomId;
this.sockets.set(client.userId, client); this.sockets.set(client.userId, client);
// Let's log server load when a user joins // Let's log server load when a user joins
this.nbClientsGauge.inc(); this.nbClientsGauge.inc();
console.log(new Date().toISOString() + ' A user joined (', this.sockets.size, ' connected users)'); console.log(new Date().toISOString() + ' A user joined (', this.sockets.size, ' connected users)');
// Let's join the room
this.handleJoinRoom(client, client.roomId, client.position, client.viewport, client.name, client.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);
}
}, },
message: (ws, arrayBuffer, isBinary) => { message: (ws, arrayBuffer, isBinary): void => {
const client = ws as ExSocketInterface; const client = ws as ExSocketInterface;
const message = ClientToServerMessage.deserializeBinary(new Uint8Array(arrayBuffer)); const message = ClientToServerMessage.deserializeBinary(new Uint8Array(arrayBuffer));
if (message.hasJoinroommessage()) { if (message.hasViewportmessage()) {
this.handleJoinRoom(client, message.getJoinroommessage() as JoinRoomMessage);
} else if (message.hasViewportmessage()) {
this.handleViewport(client, message.getViewportmessage() as ViewportMessage); this.handleViewport(client, message.getViewportmessage() as ViewportMessage);
} else if (message.hasUsermovesmessage()) { } else if (message.hasUsermovesmessage()) {
this.handleUserMovesMessage(client, message.getUsermovesmessage() as UserMovesMessage); this.handleUserMovesMessage(client, message.getUsermovesmessage() as UserMovesMessage);
@ -333,26 +312,12 @@ export class IoSocketController {
console.warn(message); console.warn(message);
} }
private handleJoinRoom(Client: ExSocketInterface, message: JoinRoomMessage): void { private handleJoinRoom(client: ExSocketInterface, roomId: string, position: PointInterface, viewport: ViewportInterface, name: string, characterLayers: string[]): void {
try { 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;
}
//leave previous room
//this.leaveRoom(Client); // Useless now, there is only one room per connection
//join new previous room //join new previous room
const world = this.joinRoom(Client, roomId, ProtobufUtils.toPointInterface(message.getPosition() as PositionMessage)); const gameRoom = this.joinRoom(client, roomId, position);
const things = world.setViewport(Client, (message.getViewport() as ViewportMessage).toObject()); const things = gameRoom.setViewport(client, viewport);
const roomJoinedMessage = new RoomJoinedMessage(); const roomJoinedMessage = new RoomJoinedMessage();
@ -382,7 +347,7 @@ export class IoSocketController {
} }
} }
for (const [itemId, item] of world.getItemsState().entries()) { for (const [itemId, item] of gameRoom.getItemsState().entries()) {
const itemStateMessage = new ItemStateMessage(); const itemStateMessage = new ItemStateMessage();
itemStateMessage.setItemid(itemId); itemStateMessage.setItemid(itemId);
itemStateMessage.setStatejson(JSON.stringify(item)); itemStateMessage.setStatejson(JSON.stringify(item));
@ -393,8 +358,8 @@ export class IoSocketController {
const serverToClientMessage = new ServerToClientMessage(); const serverToClientMessage = new ServerToClientMessage();
serverToClientMessage.setRoomjoinedmessage(roomJoinedMessage); serverToClientMessage.setRoomjoinedmessage(roomJoinedMessage);
if (!Client.disconnecting) { if (!client.disconnecting) {
Client.send(serverToClientMessage.serializeBinary().buffer, true); client.send(serverToClientMessage.serializeBinary().buffer, true);
} }
} catch (e) { } catch (e) {
console.error('An error occurred on "join_room" event'); console.error('An error occurred on "join_room" event');
@ -480,6 +445,7 @@ export class IoSocketController {
} }
} }
// Useless now, will be useful again if we allow editing details in game
private handleSetPlayerDetails(client: ExSocketInterface, playerDetailsMessage: SetPlayerDetailsMessage) { private handleSetPlayerDetails(client: ExSocketInterface, playerDetailsMessage: SetPlayerDetailsMessage) {
const playerDetails = { const playerDetails = {
name: playerDetailsMessage.getName(), name: playerDetailsMessage.getName(),
@ -493,16 +459,6 @@ export class IoSocketController {
client.name = playerDetails.name; client.name = playerDetails.name;
client.characterLayers = playerDetails.characterLayers; 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) { private handleSilentMessage(client: ExSocketInterface, silentMessage: SilentMessage) {
@ -600,7 +556,7 @@ export class IoSocketController {
if(Client.roomId){ if(Client.roomId){
try { try {
//user leave previous world //user leave previous world
const world: World | undefined = this.Worlds.get(Client.roomId); const world: GameRoom | undefined = this.Worlds.get(Client.roomId);
if (world) { if (world) {
world.leave(Client); world.leave(Client);
if (world.isEmpty()) { if (world.isEmpty()) {
@ -616,17 +572,17 @@ export class IoSocketController {
} }
} }
private joinRoom(Client : ExSocketInterface, roomId: string, position: PointInterface): World { private joinRoom(client : ExSocketInterface, roomId: string, position: PointInterface): GameRoom {
//join user in room //join user in room
//Client.join(roomId);
this.nbClientsPerRoomGauge.inc({ room: roomId }); this.nbClientsPerRoomGauge.inc({ room: roomId });
Client.roomId = roomId; client.roomId = roomId;
Client.position = position; client.position = position;
//check and create new world for a room //check and create new world for a room
let world = this.Worlds.get(roomId) let world = this.Worlds.get(roomId)
if(world === undefined){ if(world === undefined){
world = new World((user1: User, group: Group) => { world = new GameRoom((user1: User, group: Group) => {
this.joinWebRtcRoom(user1, group); this.joinWebRtcRoom(user1, group);
}, (user1: User, group: Group) => { }, (user1: User, group: Group) => {
this.disConnectedUser(user1, group); this.disConnectedUser(user1, group);
@ -689,10 +645,10 @@ export class IoSocketController {
// Dispatch groups position to newly connected user // Dispatch groups position to newly connected user
world.getGroups().forEach((group: Group) => { world.getGroups().forEach((group: Group) => {
this.emitCreateUpdateGroupEvent(Client, group); this.emitCreateUpdateGroupEvent(client, group);
}); });
//join world //join world
world.join(Client, Client.position); world.join(client, client.position);
return world; return world;
} }
@ -882,7 +838,7 @@ export class IoSocketController {
} }
public getWorlds(): Map<string, World> { public getWorlds(): Map<string, GameRoom> {
return this.Worlds; return this.Worlds;
} }
} }

View File

@ -1,12 +1,10 @@
import {MessageUserPosition, Point} from "./Websocket/MessageUserPosition";
import {PointInterface} from "./Websocket/PointInterface"; import {PointInterface} from "./Websocket/PointInterface";
import {Group} from "./Group"; import {Group} from "./Group";
import {Distance} from "./Distance";
import {User} from "./User"; import {User} from "./User";
import {ExSocketInterface} from "_Model/Websocket/ExSocketInterface"; import {ExSocketInterface} from "_Model/Websocket/ExSocketInterface";
import {PositionInterface} from "_Model/PositionInterface"; import {PositionInterface} from "_Model/PositionInterface";
import {Identificable} from "_Model/Websocket/Identificable"; import {Identificable} from "_Model/Websocket/Identificable";
import {EntersCallback, LeavesCallback, MovesCallback, Zone} from "_Model/Zone"; import {EntersCallback, LeavesCallback, MovesCallback} from "_Model/Zone";
import {PositionNotifier} from "./PositionNotifier"; import {PositionNotifier} from "./PositionNotifier";
import {ViewportInterface} from "_Model/Websocket/ViewportMessage"; import {ViewportInterface} from "_Model/Websocket/ViewportMessage";
import {Movable} from "_Model/Movable"; import {Movable} from "_Model/Movable";
@ -14,7 +12,7 @@ import {Movable} from "_Model/Movable";
export type ConnectCallback = (user: User, group: Group) => void; export type ConnectCallback = (user: User, group: Group) => void;
export type DisconnectCallback = (user: User, group: Group) => void; export type DisconnectCallback = (user: User, group: Group) => void;
export class World { export class GameRoom {
private readonly minDistance: number; private readonly minDistance: number;
private readonly groupRadius: number; private readonly groupRadius: number;
@ -123,7 +121,7 @@ export class World {
} else { } else {
// If the user is part of a group: // If the user is part of a group:
// should he leave the group? // should he leave the group?
const distance = World.computeDistanceBetweenPositions(user.getPosition(), user.group.getPosition()); const distance = GameRoom.computeDistanceBetweenPositions(user.getPosition(), user.group.getPosition());
if (distance > this.groupRadius) { if (distance > this.groupRadius) {
this.leaveGroup(user); this.leaveGroup(user);
} }
@ -199,53 +197,19 @@ export class World {
return; return;
} }
const distance = World.computeDistance(user, currentUser); // compute distance between peers. const distance = GameRoom.computeDistance(user, currentUser); // compute distance between peers.
if(distance <= minimumDistanceFound && distance <= this.minDistance) { if(distance <= minimumDistanceFound && distance <= this.minDistance) {
minimumDistanceFound = distance; minimumDistanceFound = distance;
matchingItem = currentUser; matchingItem = currentUser;
} }
/*if (typeof currentUser.group === 'undefined' || !currentUser.group.isFull()) {
// We found a user we can bind to.
return;
}*/
/*
if(context.groups.length > 0) {
context.groups.forEach(group => {
if(group.isPartOfGroup(userPosition)) { // Is the user in a group ?
if(group.isStillIn(userPosition)) { // Is the user leaving the group ? (is the user at more than max distance of each player)
// Should we split the group? (is each player reachable from the current player?)
// This is needed if
// A <==> B <==> C <===> D
// becomes A <==> B <=====> C <> D
// If C moves right, the distance between B and C is too great and we must form 2 groups
}
} else {
// If the user is in no group
// Is there someone in a group close enough and with room in the group ?
}
});
} else {
// Aucun groupe n'existe donc je stock les users assez proches de moi
let dist: Distance = {
distance: distance,
first: userPosition,
second: user // TODO: convertir en messageUserPosition
}
usersToBeGroupedWith.push(dist);
}
*/
}); });
this.groups.forEach((group: Group) => { this.groups.forEach((group: Group) => {
if (group.isFull()) { if (group.isFull()) {
return; return;
} }
const distance = World.computeDistanceBetweenPositions(user.getPosition(), group.getPosition()); const distance = GameRoom.computeDistanceBetweenPositions(user.getPosition(), group.getPosition());
if(distance <= minimumDistanceFound && distance <= this.groupRadius) { if(distance <= minimumDistanceFound && distance <= this.groupRadius) {
minimumDistanceFound = distance; minimumDistanceFound = distance;
matchingItem = group; matchingItem = group;
@ -275,66 +239,7 @@ export class World {
return this.itemsState; return this.itemsState;
} }
/*getDistancesBetweenGroupUsers(group: Group): Distance[]
{
let i = 0;
let users = group.getUsers();
let distances: Distance[] = [];
users.forEach(function(user1, key1) {
users.forEach(function(user2, key2) {
if(key1 < key2) {
distances[i] = {
distance: World.computeDistance(user1, user2),
first: user1,
second: user2
};
i++;
}
});
});
distances.sort(World.compareDistances);
return distances;
}
filterGroup(distances: Distance[], group: Group): void
{
let users = group.getUsers();
let usersToRemove = false;
let groupTmp: MessageUserPosition[] = [];
distances.forEach(dist => {
if(dist.distance <= World.MIN_DISTANCE) {
let users = [dist.first];
let usersbis = [dist.second]
groupTmp.push(dist.first);
groupTmp.push(dist.second);
} else {
usersToRemove = true;
}
});
if(usersToRemove) {
// Detecte le ou les users qui se sont fait sortir du groupe
let difference = users.filter(x => !groupTmp.includes(x));
// TODO : Notify users un difference that they have left the group
}
let newgroup = new Group(groupTmp);
this.groups.push(newgroup);
}
private static compareDistances(distA: Distance, distB: Distance): number
{
if (distA.distance < distB.distance) {
return -1;
}
if (distA.distance > distB.distance) {
return 1;
}
return 0;
}*/
setViewport(socket : Identificable, viewport: ViewportInterface): Movable[] { setViewport(socket : Identificable, viewport: ViewportInterface): Movable[] {
const user = this.users.get(socket.userId); const user = this.users.get(socket.userId);
if(typeof user === 'undefined') { if(typeof user === 'undefined') {

View File

@ -1,4 +1,4 @@
import { ConnectCallback, DisconnectCallback } from "./World"; import { ConnectCallback, DisconnectCallback } from "./GameRoom";
import { User } from "./User"; import { User } from "./User";
import {PositionInterface} from "_Model/PositionInterface"; import {PositionInterface} from "_Model/PositionInterface";
import {Movable} from "_Model/Movable"; import {Movable} from "_Model/Movable";

View File

@ -0,0 +1,41 @@
import {ADMIN_API_TOKEN, ADMIN_API_URL} from "../Enum/EnvironmentVariable";
import Axios, {AxiosError} from "axios";
export interface AdminApiData {
organizationSlug: string
worldSlug: string
roomSlug: string
mapUrlStart: string
userUuid: string
}
class AdminApi {
async fetchMemberDataByToken(organizationMemberToken: string): Promise<AdminApiData> {
if (!ADMIN_API_URL) {
return Promise.reject('No admin backoffice set!');
}
//todo: this call can fail if the corresponding world is not activated or if the token is invalid. Handle that case.
const res = await Axios.get(ADMIN_API_URL+'/api/login-url/'+organizationMemberToken,
{ headers: {"Authorization" : `${ADMIN_API_TOKEN}`} }
)
return res.data;
}
async memberIsGrantedAccessToRoom(memberId: string, roomId: string): Promise<boolean> {
if (!ADMIN_API_URL) {
return Promise.reject('No admin backoffice set!');
}
try {
const res = await Axios.get(ADMIN_API_URL+'/api/member/is-granted-access',
{ headers: {"Authorization" : `${ADMIN_API_TOKEN}`}, params: {memberId, roomIdentifier: roomId} }
)
return !!res.data;
} catch (e) {
console.log(e.message)
return false;
}
}
}
export const adminApi = new AdminApi();

View File

@ -0,0 +1,60 @@
import {ALLOW_ARTILLERY, SECRET_KEY} from "../Enum/EnvironmentVariable";
import {uuid} from "uuidv4";
import Jwt from "jsonwebtoken";
import {TokenInterface} from "../Controller/AuthenticateController";
class JWTTokenManager {
public createJWTToken(userUuid: string) {
return Jwt.sign({userUuid: userUuid}, SECRET_KEY, {expiresIn: '24h'});
}
public async getUserUuidFromToken(token: unknown): Promise<string> {
if (!token) {
throw new Error('An authentication error happened, a user tried to connect without a token.');
}
if (typeof(token) !== "string") {
throw new Error('Token is expected to be a string');
}
if(token === 'test') {
if (ALLOW_ARTILLERY) {
return uuid();
} 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'");
}
}
return new Promise<string>((resolve, reject) => {
Jwt.verify(token, SECRET_KEY, {},(err, tokenDecoded) => {
const tokenInterface = tokenDecoded as TokenInterface;
if (err) {
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;
}
if (!this.isValidToken(tokenInterface)) {
reject(new Error('Authentication error, invalid token structure.'));
return;
}
resolve(tokenInterface.userUuid);
});
});
}
private isValidToken(token: object): token is TokenInterface {
return !(typeof((token as TokenInterface).userUuid) !== 'string');
}
}
export const jwtTokenManager = new JWTTokenManager();

View File

@ -1,5 +1,5 @@
import "jasmine"; import "jasmine";
import {World, ConnectCallback, DisconnectCallback } from "../src/Model/World"; import {GameRoom, ConnectCallback, DisconnectCallback } from "_Model/GameRoom";
import {Point} from "../src/Model/Websocket/MessageUserPosition"; import {Point} from "../src/Model/Websocket/MessageUserPosition";
import { Group } from "../src/Model/Group"; import { Group } from "../src/Model/Group";
import {PositionNotifier} from "../src/Model/PositionNotifier"; import {PositionNotifier} from "../src/Model/PositionNotifier";

View File

@ -1,5 +1,5 @@
import "jasmine"; import "jasmine";
import {World, ConnectCallback, DisconnectCallback } from "../src/Model/World"; import {GameRoom, ConnectCallback, DisconnectCallback } from "../src/Model/GameRoom";
import {Point} from "../src/Model/Websocket/MessageUserPosition"; import {Point} from "../src/Model/Websocket/MessageUserPosition";
import { Group } from "../src/Model/Group"; import { Group } from "../src/Model/Group";
import {ExSocketInterface} from "_Model/Websocket/ExSocketInterface"; import {ExSocketInterface} from "_Model/Websocket/ExSocketInterface";
@ -21,7 +21,7 @@ describe("World", () => {
} }
const world = new World(connect, disconnect, 160, 160, () => {}, () => {}, () => {}); const world = new GameRoom(connect, disconnect, 160, 160, () => {}, () => {}, () => {});
world.join(createMockUser(1), new Point(100, 100)); world.join(createMockUser(1), new Point(100, 100));
@ -48,7 +48,7 @@ describe("World", () => {
} }
const world = new World(connect, disconnect, 160, 160, () => {}, () => {}, () => {}); const world = new GameRoom(connect, disconnect, 160, 160, () => {}, () => {}, () => {});
world.join(createMockUser(1), new Point(100, 100)); world.join(createMockUser(1), new Point(100, 100));
@ -77,7 +77,7 @@ describe("World", () => {
disconnectCallNumber++; disconnectCallNumber++;
} }
const world = new World(connect, disconnect, 160, 160, () => {}, () => {}, () => {}); const world = new GameRoom(connect, disconnect, 160, 160, () => {}, () => {}, () => {});
world.join(createMockUser(1), new Point(100, 100)); world.join(createMockUser(1), new Point(100, 100));

View File

@ -27,7 +27,7 @@
"google-protobuf": "^3.13.0", "google-protobuf": "^3.13.0",
"phaser": "^3.22.0", "phaser": "^3.22.0",
"queue-typescript": "^1.0.1", "queue-typescript": "^1.0.1",
"quill": "1.3.7", "quill": "^1.3.7",
"simple-peer": "^9.6.2", "simple-peer": "^9.6.2",
"socket.io-client": "^2.3.0", "socket.io-client": "^2.3.0",
"webpack-require-http": "^0.4.3" "webpack-require-http": "^0.4.3"

View File

@ -1,6 +1,7 @@
import Axios from "axios"; import Axios from "axios";
import {API_URL} from "../Enum/EnvironmentVariable"; import {API_URL} from "../Enum/EnvironmentVariable";
import {RoomConnection} from "./RoomConnection"; import {RoomConnection} from "./RoomConnection";
import {PositionInterface, ViewportInterface} from "./ConnexionModels";
interface LoginApiData { interface LoginApiData {
authToken: string authToken: string
@ -16,15 +17,26 @@ class ConnectionManager {
private authToken:string|null = null; private authToken:string|null = null;
private userUuid: string|null = null; private userUuid: string|null = null;
//todo: get map infos from url in anonym case
public async init(): Promise<void> { public async init(): Promise<void> {
let organizationMemberToken = null;
let teamSlug = null;
let mapSlug = null;
const match = /\/register\/(.+)/.exec(window.location.toString()); const match = /\/register\/(.+)/.exec(window.location.toString());
const organizationMemberToken = match ? match[1] : null; if (match) {
this.initPromise = Axios.post(`${API_URL}/login`, {organizationMemberToken}).then(res => res.data); organizationMemberToken = match[1];
} else {
const match = /\/_\/(.+)\/(.+)/.exec(window.location.toString());
teamSlug = match ? match[1] : null;
mapSlug = match ? match[2] : null;
}
this.initPromise = Axios.post(`${API_URL}/login`, {organizationMemberToken, teamSlug, mapSlug}).then(res => res.data);
const data = await this.initPromise const data = await this.initPromise
this.authToken = data.authToken; this.authToken = data.authToken;
this.userUuid = data.userUuid; this.userUuid = data.userUuid;
this.mapUrlStart = data.mapUrlStart; this.mapUrlStart = data.mapUrlStart;
const newUrl = data.newUrl; const newUrl = data.newUrl;
console.log('u', this.userUuid)
if (newUrl) { if (newUrl) {
history.pushState({}, '', newUrl); history.pushState({}, '', newUrl);
@ -35,9 +47,9 @@ class ConnectionManager {
this.authToken = 'test'; this.authToken = 'test';
} }
public connectToRoomSocket(): Promise<RoomConnection> { public connectToRoomSocket(roomId: string, name: string, characterLayers: string[], position: PositionInterface, viewport: ViewportInterface): Promise<RoomConnection> {
return new Promise<RoomConnection>((resolve, reject) => { return new Promise<RoomConnection>((resolve, reject) => {
const connection = new RoomConnection(this.authToken as string); const connection = new RoomConnection(this.authToken, roomId, name, characterLayers, position, viewport);
connection.onConnectError((error: object) => { connection.onConnectError((error: object) => {
console.log('An error occurred while connecting to socket server. Retrying'); console.log('An error occurred while connecting to socket server. Retrying');
reject(error); reject(error);
@ -50,7 +62,7 @@ class ConnectionManager {
return new Promise<RoomConnection>((resolve, reject) => { return new Promise<RoomConnection>((resolve, reject) => {
setTimeout(() => { setTimeout(() => {
//todo: allow a way to break recurrsion? //todo: allow a way to break recurrsion?
this.connectToRoomSocket().then((connection) => resolve(connection)); this.connectToRoomSocket(roomId, name, characterLayers, position, viewport).then((connection) => resolve(connection));
}, 4000 + Math.floor(Math.random() * 2000) ); }, 4000 + Math.floor(Math.random() * 2000) );
}); });
}); });

View File

@ -6,6 +6,7 @@ export enum EventMessage{
WEBRTC_SIGNAL = "webrtc-signal", WEBRTC_SIGNAL = "webrtc-signal",
WEBRTC_SCREEN_SHARING_SIGNAL = "webrtc-screen-sharing-signal", WEBRTC_SCREEN_SHARING_SIGNAL = "webrtc-screen-sharing-signal",
WEBRTC_START = "webrtc-start", WEBRTC_START = "webrtc-start",
START_ROOM = "start-room", // From server to client: list of all room users/groups/items
JOIN_ROOM = "join-room", // bi-directional JOIN_ROOM = "join-room", // bi-directional
USER_POSITION = "user-position", // From client to server USER_POSITION = "user-position", // From client to server
USER_MOVED = "user-moved", // From server to client USER_MOVED = "user-moved", // From server to client

View File

@ -6,7 +6,7 @@ import {
GroupDeleteMessage, GroupDeleteMessage,
GroupUpdateMessage, GroupUpdateMessage,
ItemEventMessage, ItemEventMessage,
JoinRoomMessage, PlayGlobalMessage, PlayGlobalMessage,
PositionMessage, PositionMessage,
RoomJoinedMessage, RoomJoinedMessage,
ServerToClientMessage, ServerToClientMessage,
@ -30,7 +30,7 @@ import {ProtobufClientUtils} from "../Network/ProtobufClientUtils";
import { import {
EventMessage, EventMessage,
GroupCreatedUpdatedMessageInterface, ItemEventMessageInterface, GroupCreatedUpdatedMessageInterface, ItemEventMessageInterface,
MessageUserJoined, PlayGlobalMessageInterface, MessageUserJoined, PlayGlobalMessageInterface, PositionInterface,
RoomJoinedMessageInterface, RoomJoinedMessageInterface,
ViewportInterface, WebRtcDisconnectMessageInterface, ViewportInterface, WebRtcDisconnectMessageInterface,
WebRtcSignalReceivedMessageInterface, WebRtcSignalReceivedMessageInterface,
@ -49,9 +49,25 @@ export class RoomConnection implements RoomConnection {
RoomConnection.websocketFactory = websocketFactory; RoomConnection.websocketFactory = websocketFactory;
} }
public constructor(token: string) { /**
*
* @param token A JWT token containing the UUID of the user
* @param roomId The ID of the room in the form "_/[instance]/[map_url]" or "@/[org]/[event]/[map]"
*/
public constructor(token: string|null, roomId: string, name: string, characterLayers: string[], position: PositionInterface, viewport: ViewportInterface) {
let url = API_URL.replace('http://', 'ws://').replace('https://', 'wss://'); let url = API_URL.replace('http://', 'ws://').replace('https://', 'wss://');
url += '?token='+token; url += '/room/'+roomId
url += '?token='+(token ?encodeURIComponent(token):'');
url += '&name='+encodeURIComponent(name);
for (const layer of characterLayers) {
url += '&characterLayers='+encodeURIComponent(layer);
}
url += '&x='+Math.floor(position.x);
url += '&y='+Math.floor(position.y);
url += '&top='+Math.floor(viewport.top);
url += '&bottom='+Math.floor(viewport.bottom);
url += '&left='+Math.floor(viewport.left);
url += '&right='+Math.floor(viewport.right);
if (RoomConnection.websocketFactory) { if (RoomConnection.websocketFactory) {
this.socket = RoomConnection.websocketFactory(url); this.socket = RoomConnection.websocketFactory(url);
@ -107,11 +123,11 @@ export class RoomConnection implements RoomConnection {
items[item.getItemid()] = JSON.parse(item.getStatejson()); items[item.getItemid()] = JSON.parse(item.getStatejson());
} }
this.resolveJoinRoom({ this.dispatch(EventMessage.START_ROOM, {
users, users,
groups, groups,
items items
}) });
} else if (message.hasSetuseridmessage()) { } else if (message.hasSetuseridmessage()) {
this.userId = (message.getSetuseridmessage() as SetUserIdMessage).getUserid(); this.userId = (message.getSetuseridmessage() as SetUserIdMessage).getUserid();
} else if (message.hasErrormessage()) { } else if (message.hasErrormessage()) {
@ -161,29 +177,6 @@ export class RoomConnection implements RoomConnection {
this.closed = true; this.closed = true;
} }
private resolveJoinRoom!: (value?: (RoomJoinedMessageInterface | PromiseLike<RoomJoinedMessageInterface> | undefined)) => void;
public joinARoom(roomId: string, startX: number, startY: number, direction: string, moving: boolean, viewport: ViewportInterface): Promise<RoomJoinedMessageInterface> {
const promise = new Promise<RoomJoinedMessageInterface>((resolve, reject) => {
this.resolveJoinRoom = resolve;
const positionMessage = this.toPositionMessage(startX, startY, direction, moving);
const viewportMessage = this.toViewportMessage(viewport);
const joinRoomMessage = new JoinRoomMessage();
joinRoomMessage.setRoomid(roomId);
joinRoomMessage.setPosition(positionMessage);
joinRoomMessage.setViewport(viewportMessage);
//console.log('Sending position ', positionMessage.getX(), positionMessage.getY());
const clientToServerMessage = new ClientToServerMessage();
clientToServerMessage.setJoinroommessage(joinRoomMessage);
this.socket.send(clientToServerMessage.serializeBinary().buffer);
})
return promise;
}
private toPositionMessage(x : number, y : number, direction : string, moving: boolean): PositionMessage { private toPositionMessage(x : number, y : number, direction : string, moving: boolean): PositionMessage {
const positionMessage = new PositionMessage(); const positionMessage = new PositionMessage();
positionMessage.setX(Math.floor(x)); positionMessage.setX(Math.floor(x));
@ -339,6 +332,13 @@ export class RoomConnection implements RoomConnection {
this.socket.addEventListener('open', callback) this.socket.addEventListener('open', callback)
} }
/**
* Triggered when we receive all the details of a room (users, groups, ...)
*/
public onStartRoom(callback: (event: RoomJoinedMessageInterface) => void): void {
this.onMessage(EventMessage.START_ROOM, callback);
}
public sendWebrtcSignal(signal: unknown, receiverId: number) { public sendWebrtcSignal(signal: unknown, receiverId: number) {
const webRtcSignal = new WebRtcSignalToServerMessage(); const webRtcSignal = new WebRtcSignalToServerMessage();
webRtcSignal.setReceiverid(receiverId); webRtcSignal.setReceiverid(receiverId);

View File

@ -1,4 +1,4 @@
import {GameScene} from "./GameScene"; import {GameScene, GameSceneInitInterface} from "./GameScene";
import { import {
StartMapInterface StartMapInterface
} from "../../Connexion/ConnexionModels"; } from "../../Connexion/ConnexionModels";
@ -13,6 +13,11 @@ export interface HasMovedEvent {
y: number; y: number;
} }
export interface loadMapResponseInterface {
key: string,
startLayerName: string;
}
export class GameManager { export class GameManager {
private playerName!: string; private playerName!: string;
private characterLayers!: string[]; private characterLayers!: string[];
@ -29,15 +34,6 @@ export class GameManager {
this.characterLayers = layers; this.characterLayers = layers;
} }
loadStartMap() : Promise<StartMapInterface> {
return connectionManager.getMapUrlStart().then(mapUrlStart => {
return {
mapUrlStart: mapUrlStart,
startInstance: "global", //todo: is this property still usefull?
}
});
}
getPlayerName(): string { getPlayerName(): string {
return this.playerName; return this.playerName;
} }
@ -46,8 +42,47 @@ export class GameManager {
return this.characterLayers; return this.characterLayers;
} }
loadMap(mapUrl: string, scene: Phaser.Scenes.ScenePlugin, instance: string): string { /**
const sceneKey = GameScene.getMapKeyByUrl(mapUrl); * Returns the map URL and the instance from the current URL
*/
private findMapUrl(): [string, string]|null {
const path = window.location.pathname;
if (!path.startsWith('/_/')) {
return null;
}
const instanceAndMap = path.substr(3);
const firstSlash = instanceAndMap.indexOf('/');
if (firstSlash === -1) {
return null;
}
const instance = instanceAndMap.substr(0, firstSlash);
return [window.location.protocol+'//'+instanceAndMap.substr(firstSlash+1), instance];
}
public loadStartingMap(scene: Phaser.Scenes.ScenePlugin): Promise<loadMapResponseInterface> {
// Do we have a start URL in the address bar? If so, let's redirect to this address
const instanceAndMapUrl = this.findMapUrl();
if (instanceAndMapUrl !== null) {
const [mapUrl, instance] = instanceAndMapUrl;
const key = gameManager.loadMap(mapUrl, scene, instance);
const startLayerName = window.location.hash ? window.location.hash.substr(1) : '';
return Promise.resolve({key, startLayerName});
} else {
// If we do not have a map address in the URL, let's ask the server for a start map.
return connectionManager.getMapUrlStart().then((mapUrlStart: string) => {
const key = gameManager.loadMap(window.location.protocol + "//" + mapUrlStart, scene, 'global');
return {key, startLayerName: ''}
}).catch((err) => {
console.error(err);
throw err;
});
}
}
public loadMap(mapUrl: string, scene: Phaser.Scenes.ScenePlugin, instance: string): string {
const sceneKey = this.getMapKeyByUrl(mapUrl);
const gameIndex = scene.getIndex(sceneKey); const gameIndex = scene.getIndex(sceneKey);
if(gameIndex === -1){ if(gameIndex === -1){
@ -56,6 +91,13 @@ export class GameManager {
} }
return sceneKey; return sceneKey;
} }
public getMapKeyByUrl(mapUrlStart: string) : string {
// FIXME: the key should be computed from the full URL of the map.
const startPos = mapUrlStart.indexOf('://')+3;
const endPos = mapUrlStart.indexOf(".json");
return mapUrlStart.substring(startPos, endPos);
}
} }
export const gameManager = new GameManager(); export const gameManager = new GameManager();

View File

@ -108,7 +108,6 @@ export class GameScene extends ResizableScene implements CenterListener {
private simplePeer!: SimplePeer; private simplePeer!: SimplePeer;
private GlobalMessageManager!: GlobalMessageManager; private GlobalMessageManager!: GlobalMessageManager;
private ConsoleGlobalMessageManager!: ConsoleGlobalMessageManager; private ConsoleGlobalMessageManager!: ConsoleGlobalMessageManager;
private connectionPromise!: Promise<RoomConnection>
private connectionAnswerPromise: Promise<RoomJoinedMessageInterface>; private connectionAnswerPromise: Promise<RoomJoinedMessageInterface>;
private connectionAnswerPromiseResolve!: (value?: RoomJoinedMessageInterface | PromiseLike<RoomJoinedMessageInterface>) => void; private connectionAnswerPromiseResolve!: (value?: RoomJoinedMessageInterface | PromiseLike<RoomJoinedMessageInterface>) => void;
// A promise that will resolve when the "create" method is called (signaling loading is ended) // A promise that will resolve when the "create" method is called (signaling loading is ended)
@ -139,17 +138,17 @@ export class GameScene extends ResizableScene implements CenterListener {
private outlinedItem: ActionableItem|null = null; private outlinedItem: ActionableItem|null = null;
private userInputManager!: UserInputManager; private userInputManager!: UserInputManager;
static createFromUrl(mapUrlFile: string, instance: string, key: string|null = null): GameScene { static createFromUrl(mapUrlFile: string, instance: string, gameSceneKey: string|null = null): GameScene {
const mapKey = GameScene.getMapKeyByUrl(mapUrlFile); const mapKey = gameManager.getMapKeyByUrl(mapUrlFile);
if (key === null) { if (gameSceneKey === null) {
key = mapKey; gameSceneKey = mapKey;
} }
return new GameScene(mapKey, mapUrlFile, instance, key); return new GameScene(mapKey, mapUrlFile, instance, gameSceneKey);
} }
constructor(MapKey : string, MapUrlFile: string, instance: string, key: string) { constructor(MapKey : string, MapUrlFile: string, instance: string, gameSceneKey: string) {
super({ super({
key: key key: gameSceneKey
}); });
this.GameManager = gameManager; this.GameManager = gameManager;
@ -206,105 +205,6 @@ export class GameScene extends ResizableScene implements CenterListener {
loadAllLayers(this.load); loadAllLayers(this.load);
this.load.bitmapFont('main_font', 'resources/fonts/arcade.png', 'resources/fonts/arcade.xml'); this.load.bitmapFont('main_font', 'resources/fonts/arcade.png', 'resources/fonts/arcade.xml');
this.connectionPromise = connectionManager.connectToRoomSocket().then((connection : RoomConnection) => {
this.connection = connection;
this.connection.emitPlayerDetailsMessage(gameManager.getPlayerName(), gameManager.getCharacterSelected())
connection.onUserJoins((message: MessageUserJoined) => {
const userMessage: AddPlayerInterface = {
userId: message.userId,
characterLayers: message.characterLayers,
name: message.name,
position: message.position
}
this.addPlayer(userMessage);
});
connection.onUserMoved((message: UserMovedMessage) => {
const position = message.getPosition();
if (position === undefined) {
throw new Error('Position missing from UserMovedMessage');
}
//console.log('Received position ', position.getX(), position.getY(), "from user", message.getUserid());
const messageUserMoved: MessageUserMovedInterface = {
userId: message.getUserid(),
position: ProtobufClientUtils.toPointInterface(position)
}
this.updatePlayerPosition(messageUserMoved);
});
connection.onUserLeft((userId: number) => {
this.removePlayer(userId);
});
connection.onGroupUpdatedOrCreated((groupPositionMessage: GroupCreatedUpdatedMessageInterface) => {
this.shareGroupPosition(groupPositionMessage);
})
connection.onGroupDeleted((groupId: number) => {
try {
this.deleteGroup(groupId);
} catch (e) {
console.error(e);
}
})
connection.onServerDisconnected(() => {
console.log('Player disconnected from server. Reloading scene.');
this.simplePeer.closeAllConnections();
this.simplePeer.unregister();
const key = 'somekey'+Math.round(Math.random()*10000);
const game : Phaser.Scene = GameScene.createFromUrl(this.MapUrlFile, this.instance, key);
this.scene.add(key, game, true,
{
initPosition: {
x: this.CurrentPlayer.x,
y: this.CurrentPlayer.y
}
});
this.scene.stop(this.scene.key);
this.scene.remove(this.scene.key);
})
connection.onActionableEvent((message => {
const item = this.actionableItems.get(message.itemId);
if (item === undefined) {
console.warn('Received an event about object "'+message.itemId+'" but cannot find this item on the map.');
return;
}
item.fire(message.event, message.state, message.parameters);
}));
// When connection is performed, let's connect SimplePeer
this.simplePeer = new SimplePeer(this.connection);
this.GlobalMessageManager = new GlobalMessageManager(this.connection);
const self = this;
this.simplePeer.registerPeerConnectionListener({
onConnect(user: UserSimplePeerInterface) {
self.presentationModeSprite.setVisible(true);
self.chatModeSprite.setVisible(true);
},
onDisconnect(userId: number) {
if (self.simplePeer.getNbConnections() === 0) {
self.presentationModeSprite.setVisible(false);
self.chatModeSprite.setVisible(false);
}
}
})
this.scene.wake();
this.scene.sleep(ReconnectingSceneName);
return connection;
});
} }
// FIXME: we need to put a "unknown" instead of a "any" and validate the structure of the JSON we are receiving. // FIXME: we need to put a "unknown" instead of a "any" and validate the structure of the JSON we are receiving.
@ -614,6 +514,132 @@ export class GameScene extends ResizableScene implements CenterListener {
this.connection.setSilent(true); this.connection.setSilent(true);
} }
}); });
const camera = this.cameras.main;
connectionManager.connectToRoomSocket(
this.RoomId,
gameManager.getPlayerName(),
gameManager.getCharacterSelected(),
{
x: this.startX,
y: this.startY
},
{
left: camera.scrollX,
top: camera.scrollY,
right: camera.scrollX + camera.width,
bottom: camera.scrollY + camera.height,
}).then((connection : RoomConnection) => {
this.connection = connection;
//this.connection.emitPlayerDetailsMessage(gameManager.getPlayerName(), gameManager.getCharacterSelected())
connection.onStartRoom((roomJoinedMessage: RoomJoinedMessageInterface) => {
this.initUsersPosition(roomJoinedMessage.users);
this.connectionAnswerPromiseResolve(roomJoinedMessage);
});
connection.onUserJoins((message: MessageUserJoined) => {
const userMessage: AddPlayerInterface = {
userId: message.userId,
characterLayers: message.characterLayers,
name: message.name,
position: message.position
}
this.addPlayer(userMessage);
});
connection.onUserMoved((message: UserMovedMessage) => {
const position = message.getPosition();
if (position === undefined) {
throw new Error('Position missing from UserMovedMessage');
}
//console.log('Received position ', position.getX(), position.getY(), "from user", message.getUserid());
const messageUserMoved: MessageUserMovedInterface = {
userId: message.getUserid(),
position: ProtobufClientUtils.toPointInterface(position)
}
this.updatePlayerPosition(messageUserMoved);
});
connection.onUserLeft((userId: number) => {
this.removePlayer(userId);
});
connection.onGroupUpdatedOrCreated((groupPositionMessage: GroupCreatedUpdatedMessageInterface) => {
this.shareGroupPosition(groupPositionMessage);
})
connection.onGroupDeleted((groupId: number) => {
try {
this.deleteGroup(groupId);
} catch (e) {
console.error(e);
}
})
connection.onServerDisconnected(() => {
console.log('Player disconnected from server. Reloading scene.');
this.simplePeer.closeAllConnections();
this.simplePeer.unregister();
const gameSceneKey = 'somekey'+Math.round(Math.random()*10000);
const game : Phaser.Scene = GameScene.createFromUrl(this.MapUrlFile, this.instance, gameSceneKey);
this.scene.add(gameSceneKey, game, true,
{
initPosition: {
x: this.CurrentPlayer.x,
y: this.CurrentPlayer.y
}
});
this.scene.stop(this.scene.key);
this.scene.remove(this.scene.key);
})
connection.onActionableEvent((message => {
const item = this.actionableItems.get(message.itemId);
if (item === undefined) {
console.warn('Received an event about object "'+message.itemId+'" but cannot find this item on the map.');
return;
}
item.fire(message.event, message.state, message.parameters);
}));
// When connection is performed, let's connect SimplePeer
this.simplePeer = new SimplePeer(this.connection);
this.GlobalMessageManager = new GlobalMessageManager(this.connection);
const self = this;
this.simplePeer.registerPeerConnectionListener({
onConnect(user: UserSimplePeerInterface) {
self.presentationModeSprite.setVisible(true);
self.chatModeSprite.setVisible(true);
},
onDisconnect(userId: number) {
if (self.simplePeer.getNbConnections() === 0) {
self.presentationModeSprite.setVisible(false);
self.chatModeSprite.setVisible(false);
}
}
})
//listen event to share position of user
this.CurrentPlayer.on(hasMovedEventName, this.pushPlayerPosition.bind(this))
this.CurrentPlayer.on(hasMovedEventName, this.outlineItem.bind(this))
this.CurrentPlayer.on(hasMovedEventName, (event: HasMovedEvent) => {
this.gameMap.setPosition(event.x, event.y);
})
this.scene.wake();
this.scene.sleep(ReconnectingSceneName);
return connection;
});
} }
private switchLayoutMode(): void { private switchLayoutMode(): void {
@ -784,32 +810,6 @@ export class GameScene extends ResizableScene implements CenterListener {
//create collision //create collision
this.createCollisionWithPlayer(); this.createCollisionWithPlayer();
this.createCollisionObject(); this.createCollisionObject();
//join room
this.connectionPromise.then((connection: RoomConnection) => {
const camera = this.cameras.main;
connection.joinARoom(this.RoomId,
this.startX,
this.startY,
PlayerAnimationNames.WalkDown,
false, {
left: camera.scrollX,
top: camera.scrollY,
right: camera.scrollX + camera.width,
bottom: camera.scrollY + camera.height,
}).then((roomJoinedMessage: RoomJoinedMessageInterface) => {
this.initUsersPosition(roomJoinedMessage.users);
this.connectionAnswerPromiseResolve(roomJoinedMessage);
});
// FIXME: weirdly enough we don't use the result of joinARoom !!!!!!
//listen event to share position of user
this.CurrentPlayer.on(hasMovedEventName, this.pushPlayerPosition.bind(this))
this.CurrentPlayer.on(hasMovedEventName, this.outlineItem.bind(this))
this.CurrentPlayer.on(hasMovedEventName, (event: HasMovedEvent) => {
this.gameMap.setPosition(event.x, event.y);
})
});
} }
pushPlayerPosition(event: HasMovedEvent) { pushPlayerPosition(event: HasMovedEvent) {
@ -979,7 +979,6 @@ export class GameScene extends ResizableScene implements CenterListener {
type: "InitUserPositionEvent", type: "InitUserPositionEvent",
event: usersPosition event: usersPosition
}); });
} }
/** /**
@ -1133,12 +1132,7 @@ export class GameScene extends ResizableScene implements CenterListener {
this.groups.delete(groupId); this.groups.delete(groupId);
} }
public static getMapKeyByUrl(mapUrlStart: string) : string {
// FIXME: the key should be computed from the full URL of the map.
const startPos = mapUrlStart.indexOf('://')+3;
const endPos = mapUrlStart.indexOf(".json");
return mapUrlStart.substring(startPos, endPos);
}
/** /**
* Sends to the server an event emitted by one of the ActionableItems. * Sends to the server an event emitted by one of the ActionableItems.

View File

@ -94,7 +94,7 @@ export class EnableCameraScene extends Phaser.Scene {
this.add.existing(this.logo); this.add.existing(this.logo);
this.input.keyboard.on('keyup-ENTER', () => { this.input.keyboard.on('keyup-ENTER', () => {
return this.login(); this.login();
}); });
this.getElementByIdOrFail<HTMLDivElement>('webRtcSetup').classList.add('active'); this.getElementByIdOrFail<HTMLDivElement>('webRtcSetup').classList.add('active');
@ -258,7 +258,7 @@ export class EnableCameraScene extends Phaser.Scene {
this.soundMeterSprite.setVolume(this.soundMeter.getVolume()); this.soundMeterSprite.setVolume(this.soundMeter.getVolume());
} }
private async login(): Promise<StartMapInterface> { private async login(): Promise<void> {
this.getElementByIdOrFail<HTMLDivElement>('webRtcSetup').style.display = 'none'; this.getElementByIdOrFail<HTMLDivElement>('webRtcSetup').style.display = 'none';
this.soundMeter.stop(); this.soundMeter.stop();
window.removeEventListener('resize', this.repositionCallback); window.removeEventListener('resize', this.repositionCallback);
@ -266,46 +266,8 @@ export class EnableCameraScene extends Phaser.Scene {
mediaManager.stopCamera(); mediaManager.stopCamera();
mediaManager.stopMicrophone(); mediaManager.stopMicrophone();
// Do we have a start URL in the address bar? If so, let's redirect to this address const {key, startLayerName} = await gameManager.loadStartingMap(this.scene);
const instanceAndMapUrl = this.findMapUrl(); this.scene.start(key, {startLayerName});
if (instanceAndMapUrl !== null) {
const [mapUrl, instance] = instanceAndMapUrl;
const key = gameManager.loadMap(mapUrl, this.scene, instance);
this.scene.start(key, {
startLayerName: window.location.hash ? window.location.hash.substr(1) : undefined
} as GameSceneInitInterface);
return {
mapUrlStart: mapUrl,
startInstance: instance
};
} else {
// If we do not have a map address in the URL, let's ask the server for a start map.
return gameManager.loadStartMap().then((startMap: StartMapInterface) => {
const key = gameManager.loadMap(window.location.protocol + "//" + startMap.mapUrlStart, this.scene, startMap.startInstance);
this.scene.start(key);
return startMap;
}).catch((err) => {
console.error(err);
throw err;
});
}
}
/**
* Returns the map URL and the instance from the current URL
*/
private findMapUrl(): [string, string]|null {
const path = window.location.pathname;
if (!path.startsWith('/_/')) {
return null;
}
const instanceAndMap = path.substr(3);
const firstSlash = instanceAndMap.indexOf('/');
if (firstSlash === -1) {
return null;
}
const instance = instanceAndMap.substr(0, firstSlash);
return [window.location.protocol+'//'+instanceAndMap.substr(firstSlash+1), instance];
} }
private async getDevices() { private async getDevices() {

View File

@ -3871,7 +3871,7 @@ quill-delta@^3.6.2:
extend "^3.0.2" extend "^3.0.2"
fast-diff "1.1.2" fast-diff "1.1.2"
quill@1.3.7: quill@^1.3.7:
version "1.3.7" version "1.3.7"
resolved "https://registry.yarnpkg.com/quill/-/quill-1.3.7.tgz#da5b2f3a2c470e932340cdbf3668c9f21f9286e8" resolved "https://registry.yarnpkg.com/quill/-/quill-1.3.7.tgz#da5b2f3a2c470e932340cdbf3668c9f21f9286e8"
integrity sha512-hG/DVzh/TiknWtE6QmWAF/pxoZKYxfe3J/d/+ShUWkDvvkZQVTPeVmUJVu1uE6DDooC4fWTiCLh84ul89oNz5g== integrity sha512-hG/DVzh/TiknWtE6QmWAF/pxoZKYxfe3J/d/+ShUWkDvvkZQVTPeVmUJVu1uE6DDooC4fWTiCLh84ul89oNz5g==

View File

@ -38,12 +38,6 @@ message SetPlayerDetailsMessage {
repeated string characterLayers = 2; repeated string characterLayers = 2;
} }
message JoinRoomMessage {
string roomId = 1;
PositionMessage position = 2;
ViewportMessage viewport = 3;
}
message UserMovesMessage { message UserMovesMessage {
PositionMessage position = 1; PositionMessage position = 1;
ViewportMessage viewport = 2; ViewportMessage viewport = 2;
@ -56,7 +50,6 @@ message WebRtcSignalToServerMessage {
message ClientToServerMessage { message ClientToServerMessage {
oneof message { oneof message {
JoinRoomMessage joinRoomMessage = 1;
UserMovesMessage userMovesMessage = 2; UserMovesMessage userMovesMessage = 2;
SilentMessage silentMessage = 3; SilentMessage silentMessage = 3;
ViewportMessage viewportMessage = 4; ViewportMessage viewportMessage = 4;