Merge branch 'develop' into windows-focus-blur-camera

# Conflicts:
#	front/src/Phaser/Game/GameScene.ts
This commit is contained in:
Gregoire Parant 2020-11-16 15:46:52 +01:00
commit 1570ef9663
11 changed files with 76 additions and 67 deletions

View File

@ -20,9 +20,9 @@ import {parse} from "query-string";
import {jwtTokenManager} from "../Services/JWTTokenManager"; import {jwtTokenManager} from "../Services/JWTTokenManager";
import {adminApi, CharacterTexture, FetchMemberDataByUuidResponse} from "../Services/AdminApi"; import {adminApi, CharacterTexture, FetchMemberDataByUuidResponse} from "../Services/AdminApi";
import {SocketManager, socketManager} from "../Services/SocketManager"; import {SocketManager, socketManager} from "../Services/SocketManager";
import {emitInBatch, pongMaxInterval, refresLogoutTimerOnPong, resetPing} from "../Services/IoSocketHelpers"; import {emitInBatch} from "../Services/IoSocketHelpers";
import {clientEventsEmitter} from "../Services/ClientEventsEmitter"; import {clientEventsEmitter} from "../Services/ClientEventsEmitter";
import {ADMIN_API_TOKEN, ADMIN_API_URL} from "../Enum/EnvironmentVariable"; import {ADMIN_API_TOKEN, ADMIN_API_URL, SOCKET_IDLE_TIMER} from "../Enum/EnvironmentVariable";
export class IoSocketController { export class IoSocketController {
private nextUserId: number = 1; private nextUserId: number = 1;
@ -110,6 +110,7 @@ export class IoSocketController {
this.app.ws('/room', { this.app.ws('/room', {
/* Options */ /* Options */
//compression: uWS.SHARED_COMPRESSOR, //compression: uWS.SHARED_COMPRESSOR,
idleTimeout: SOCKET_IDLE_TIMER,
maxPayloadLength: 16 * 1024 * 1024, maxPayloadLength: 16 * 1024 * 1024,
maxBackpressure: 65536, // Maximum 64kB of data in the buffer. maxBackpressure: 65536, // Maximum 64kB of data in the buffer.
//idleTimeout: 10, //idleTimeout: 10,
@ -239,8 +240,6 @@ export class IoSocketController {
// Let's join the room // Let's join the room
const client = this.initClient(ws); //todo: into the upgrade instead? const client = this.initClient(ws); //todo: into the upgrade instead?
socketManager.handleJoinRoom(client); socketManager.handleJoinRoom(client);
resetPing(client);
refresLogoutTimerOnPong(ws as ExSocketInterface);
//get data information and show messages //get data information and show messages
if (ADMIN_API_URL) { if (ADMIN_API_URL) {
@ -293,9 +292,6 @@ export class IoSocketController {
drain: (ws) => { drain: (ws) => {
console.log('WebSocket backpressure: ' + ws.getBufferedAmount()); console.log('WebSocket backpressure: ' + ws.getBufferedAmount());
}, },
pong(ws) {
refresLogoutTimerOnPong(ws as ExSocketInterface);
},
close: (ws, code, message) => { close: (ws, code, message) => {
const Client = (ws as ExSocketInterface); const Client = (ws as ExSocketInterface);
try { try {

View File

@ -10,6 +10,7 @@ const CPU_OVERHEAT_THRESHOLD = Number(process.env.CPU_OVERHEAT_THRESHOLD) || 80;
const JITSI_URL : string|undefined = (process.env.JITSI_URL === '') ? undefined : process.env.JITSI_URL; const JITSI_URL : string|undefined = (process.env.JITSI_URL === '') ? undefined : process.env.JITSI_URL;
const JITSI_ISS = process.env.JITSI_ISS || ''; const JITSI_ISS = process.env.JITSI_ISS || '';
const SECRET_JITSI_KEY = process.env.SECRET_JITSI_KEY || ''; const SECRET_JITSI_KEY = process.env.SECRET_JITSI_KEY || '';
export const SOCKET_IDLE_TIMER = parseInt(process.env.SOCKET_IDLE_TIMER as string) || 30; // maximum time (in second) without activity before a socket is closed
export { export {
SECRET_KEY, SECRET_KEY,

View File

@ -25,8 +25,6 @@ export interface ExSocketInterface extends WebSocket, Identificable {
emitInBatch: (payload: SubMessage) => void; emitInBatch: (payload: SubMessage) => void;
batchedMessages: BatchMessage; batchedMessages: BatchMessage;
batchTimeout: NodeJS.Timeout|null; batchTimeout: NodeJS.Timeout|null;
pingTimeout: NodeJS.Timeout|null;
pongTimeout: NodeJS.Timeout|null;
disconnecting: boolean, disconnecting: boolean,
tags: string[], tags: string[],
textures: CharacterTexture[], textures: CharacterTexture[],

View File

@ -6,8 +6,13 @@ class GaugeManager {
private nbClientsPerRoomGauge: Gauge<string>; private nbClientsPerRoomGauge: Gauge<string>;
private nbGroupsPerRoomGauge: Gauge<string>; private nbGroupsPerRoomGauge: Gauge<string>;
private nbGroupsPerRoomCounter: Counter<string>; private nbGroupsPerRoomCounter: Counter<string>;
private nbRoomsGauge: Gauge<string>;
constructor() { constructor() {
this.nbRoomsGauge = new Gauge({
name: 'workadventure_nb_rooms',
help: 'Number of active rooms'
});
this.nbClientsGauge = new Gauge({ this.nbClientsGauge = new Gauge({
name: 'workadventure_nb_sockets', name: 'workadventure_nb_sockets',
help: 'Number of connected sockets', help: 'Number of connected sockets',
@ -31,6 +36,13 @@ class GaugeManager {
}); });
} }
incNbRoomGauge(): void {
this.nbRoomsGauge.inc();
}
decNbRoomGauge(): void {
this.nbRoomsGauge.dec();
}
incNbClientPerRoomGauge(roomId: string): void { incNbClientPerRoomGauge(roomId: string): void {
this.nbClientsGauge.inc(); this.nbClientsGauge.inc();
this.nbClientsPerRoomGauge.inc({ room: roomId }); this.nbClientsPerRoomGauge.inc({ room: roomId });

View File

@ -18,22 +18,6 @@ export function emitInBatch(socket: ExSocketInterface, payload: SubMessage): voi
socket.batchTimeout = null; socket.batchTimeout = null;
}, 100); }, 100);
} }
// If we send a message, we don't need to keep the connection alive
resetPing(socket);
}
export function resetPing(ws: ExSocketInterface): void {
if (ws.pingTimeout) {
clearTimeout(ws.pingTimeout);
}
ws.pingTimeout = setTimeout(() => {
if (ws.disconnecting) {
return;
}
ws.ping();
resetPing(ws);
}, 29000);
} }
export function emitError(Client: ExSocketInterface, message: string): void { export function emitError(Client: ExSocketInterface, message: string): void {
@ -49,11 +33,3 @@ export function emitError(Client: ExSocketInterface, message: string): void {
console.warn(message); console.warn(message);
} }
export const pongMaxInterval = 30000; // the maximum duration (in ms) between pongs before we shutdown the connexion.
export function refresLogoutTimerOnPong(ws: ExSocketInterface): void {
if(ws.pongTimeout) clearTimeout(ws.pongTimeout);
ws.pongTimeout = setTimeout(() => {
ws.close();
}, pongMaxInterval);
}

View File

@ -351,6 +351,7 @@ export class SocketManager {
world.leave(Client); world.leave(Client);
if (world.isEmpty()) { if (world.isEmpty()) {
this.Worlds.delete(Client.roomId); this.Worlds.delete(Client.roomId);
gaugeManager.decNbRoomGauge();
} }
} }
//user leave previous room //user leave previous room
@ -383,6 +384,7 @@ export class SocketManager {
world.tags = data.tags world.tags = data.tags
world.policyType = Number(data.policy_type) world.policyType = Number(data.policy_type)
} }
gaugeManager.incNbRoomGauge();
this.Worlds.set(roomId, world); this.Worlds.set(roomId, world);
} }
return Promise.resolve(world) return Promise.resolve(world)

View File

@ -26,6 +26,7 @@ import {
QueryJitsiJwtMessage, QueryJitsiJwtMessage,
SendJitsiJwtMessage, SendJitsiJwtMessage,
CharacterLayerMessage, CharacterLayerMessage,
PingMessage,
SendUserMessage SendUserMessage
} from "../Messages/generated/messages_pb" } from "../Messages/generated/messages_pb"
@ -42,6 +43,8 @@ import {
} from "./ConnexionModels"; } from "./ConnexionModels";
import {BodyResourceDescriptionInterface} from "../Phaser/Entity/body_character"; import {BodyResourceDescriptionInterface} from "../Phaser/Entity/body_character";
const manualPingDelay = 20000;
export class RoomConnection implements RoomConnection { export class RoomConnection implements RoomConnection {
private readonly socket: WebSocket; private readonly socket: WebSocket;
private userId: number|null = null; private userId: number|null = null;
@ -84,7 +87,9 @@ export class RoomConnection implements RoomConnection {
this.socket.binaryType = 'arraybuffer'; this.socket.binaryType = 'arraybuffer';
this.socket.onopen = (ev) => { this.socket.onopen = (ev) => {
//console.log('WS connected'); //we manually ping every 20s to not be logged out by the server, even when the game is in background.
const pingMessage = new PingMessage();
setInterval(() => this.socket.send(pingMessage.serializeBinary().buffer), manualPingDelay);
}; };
this.socket.onmessage = (messageEvent) => { this.socket.onmessage = (messageEvent) => {

View File

@ -466,35 +466,7 @@ export class GameScene extends ResizableScene implements CenterListener {
// From now, this game scene will be notified of reposition events // From now, this game scene will be notified of reposition events
layoutManager.setListener(this); layoutManager.setListener(this);
this.triggerOnMapLayerPropertyChange();
this.gameMap.onPropertyChange('openWebsite', (newValue, oldValue) => {
if (newValue === undefined) {
coWebsiteManager.closeCoWebsite();
} else {
coWebsiteManager.loadCoWebsite(newValue as string);
}
});
this.gameMap.onPropertyChange('jitsiRoom', (newValue, oldValue, allProps) => {
if (newValue === undefined) {
this.stopJitsi();
} else {
if (JITSI_PRIVATE_MODE) {
const adminTag = allProps.get("jitsiRoomAdminTag") as string | undefined;
this.connection.emitQueryJitsiJwtMessage(this.instance.replace('/', '-') + "-" + newValue, adminTag);
} else {
this.startJitsi(newValue as string);
}
}
})
this.gameMap.onPropertyChange('silent', (newValue, oldValue) => {
if (newValue === undefined || newValue === false || newValue === '') {
this.connection.setSilent(false);
} else {
this.connection.setSilent(true);
}
});
const camera = this.cameras.main; const camera = this.cameras.main;
@ -630,10 +602,43 @@ export class GameScene extends ResizableScene implements CenterListener {
this.scene.wake(); this.scene.wake();
this.scene.sleep(ReconnectingSceneName); this.scene.sleep(ReconnectingSceneName);
//init user position and play trigger to check layers properties
this.gameMap.setPosition(this.CurrentPlayer.x, this.CurrentPlayer.y);
return connection; return connection;
}); });
} }
private triggerOnMapLayerPropertyChange(){
this.gameMap.onPropertyChange('openWebsite', (newValue, oldValue) => {
if (newValue === undefined) {
coWebsiteManager.closeCoWebsite();
} else {
coWebsiteManager.loadCoWebsite(newValue as string);
}
});
this.gameMap.onPropertyChange('jitsiRoom', (newValue, oldValue, allProps) => {
if (newValue === undefined) {
this.stopJitsi();
} else {
if (JITSI_PRIVATE_MODE) {
const adminTag = allProps.get("jitsiRoomAdminTag") as string|undefined;
this.connection.emitQueryJitsiJwtMessage(this.instance.replace('/', '-') + "-" + newValue, adminTag);
} else {
this.startJitsi(newValue as string);
}
}
})
this.gameMap.onPropertyChange('silent', (newValue, oldValue) => {
if (newValue === undefined || newValue === false || newValue === '') {
this.connection.setSilent(false);
} else {
this.connection.setSilent(true);
}
});
}
private switchLayoutMode(): void { private switchLayoutMode(): void {
const mode = layoutManager.getLayoutMode(); const mode = layoutManager.getLayoutMode();
if (mode === LayoutMode.Presentation) { if (mode === LayoutMode.Presentation) {
@ -712,6 +717,7 @@ export class GameScene extends ResizableScene implements CenterListener {
*/ */
//todo: push that into the gameManager //todo: push that into the gameManager
private loadNextGame(layer: ITiledMapLayer, mapWidth: number, roomId: string){ private loadNextGame(layer: ITiledMapLayer, mapWidth: number, roomId: string){
const room = new Room(roomId); const room = new Room(roomId);
gameManager.loadMap(room, this.scene); gameManager.loadMap(room, this.scene);
const exitSceneKey = roomId; const exitSceneKey = roomId;

View File

@ -40,7 +40,9 @@ export class UserInputManager {
initKeyBoardEvent(){ initKeyBoardEvent(){
this.KeysCode = [ this.KeysCode = [
{event: UserInputEvent.MoveUp, keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.Z, false) }, {event: UserInputEvent.MoveUp, keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.Z, false) },
{event: UserInputEvent.MoveUp, keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.W, false) },
{event: UserInputEvent.MoveLeft, keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.Q, false) }, {event: UserInputEvent.MoveLeft, keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.Q, false) },
{event: UserInputEvent.MoveLeft, keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.A, false) },
{event: UserInputEvent.MoveDown, keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.S, false) }, {event: UserInputEvent.MoveDown, keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.S, false) },
{event: UserInputEvent.MoveRight, keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.D, false) }, {event: UserInputEvent.MoveRight, keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.D, false) },

View File

@ -16,6 +16,11 @@ class CoWebsiteManager {
private opened: iframeStates = iframeStates.closed; private opened: iframeStates = iframeStates.closed;
private observers = new Array<CoWebsiteStateChangedCallback>(); private observers = new Array<CoWebsiteStateChangedCallback>();
/**
* Quickly going in and out of an iframe trigger can create conflicts between the iframe states.
* So we use this promise to queue up every cowebsite state transition
*/
private currentOperationPromise: Promise<void> = Promise.resolve();
private close(): HTMLDivElement { private close(): HTMLDivElement {
const cowebsiteDiv = HtmlUtils.getElementByIdOrFail<HTMLDivElement>(cowebsiteDivId); const cowebsiteDiv = HtmlUtils.getElementByIdOrFail<HTMLDivElement>(cowebsiteDivId);
@ -52,7 +57,7 @@ class CoWebsiteManager {
const onTimeoutPromise = new Promise((resolve) => { const onTimeoutPromise = new Promise((resolve) => {
setTimeout(() => resolve(), 2000); setTimeout(() => resolve(), 2000);
}); });
Promise.race([onloadPromise, onTimeoutPromise]).then(() => { this.currentOperationPromise = this.currentOperationPromise.then(() =>Promise.race([onloadPromise, onTimeoutPromise])).then(() => {
this.open(); this.open();
setTimeout(() => { setTimeout(() => {
this.fire(); this.fire();
@ -65,7 +70,7 @@ class CoWebsiteManager {
*/ */
public insertCoWebsite(callback: (cowebsite: HTMLDivElement) => Promise<void>): void { public insertCoWebsite(callback: (cowebsite: HTMLDivElement) => Promise<void>): void {
const cowebsiteDiv = this.load(); const cowebsiteDiv = this.load();
callback(cowebsiteDiv).then(() => { this.currentOperationPromise = this.currentOperationPromise.then(() => callback(cowebsiteDiv)).then(() => {
this.open(); this.open();
setTimeout(() => { setTimeout(() => {
this.fire(); this.fire();
@ -74,14 +79,16 @@ class CoWebsiteManager {
} }
public closeCoWebsite(): Promise<void> { public closeCoWebsite(): Promise<void> {
return new Promise((resolve, reject) => { this.currentOperationPromise = this.currentOperationPromise.then(() => new Promise((resolve, reject) => {
if(this.opened === iframeStates.closed) resolve(); //this method may be called twice, in case of iframe error for example
const cowebsiteDiv = this.close(); const cowebsiteDiv = this.close();
this.fire(); this.fire();
setTimeout(() => { setTimeout(() => {
resolve(); resolve();
setTimeout(() => cowebsiteDiv.innerHTML = '', 500) setTimeout(() => cowebsiteDiv.innerHTML = '', 500)
}, animationTime) }, animationTime)
}); }));
return this.currentOperationPromise;
} }
public getGameSize(): {width: number, height: number} { public getGameSize(): {width: number, height: number} {

View File

@ -38,6 +38,10 @@ message CharacterLayerMessage {
/*********** CLIENT TO SERVER MESSAGES *************/ /*********** CLIENT TO SERVER MESSAGES *************/
message PingMessage {
}
message SetPlayerDetailsMessage { message SetPlayerDetailsMessage {
string name = 1; string name = 1;
repeated string characterLayers = 2; repeated string characterLayers = 2;