diff --git a/back/src/Controller/IoSocketController.ts b/back/src/Controller/IoSocketController.ts index f037a99d..bb93fd50 100644 --- a/back/src/Controller/IoSocketController.ts +++ b/back/src/Controller/IoSocketController.ts @@ -22,7 +22,7 @@ import {adminApi, CharacterTexture, FetchMemberDataByUuidResponse} from "../Serv import {SocketManager, socketManager} from "../Services/SocketManager"; import {emitInBatch} from "../Services/IoSocketHelpers"; import {clientEventsEmitter} from "../Services/ClientEventsEmitter"; -import {ADMIN_API_TOKEN, ADMIN_API_URL, SOCKET_IDLE_TIMER} from "../Enum/EnvironmentVariable"; +import {ADMIN_API_TOKEN, MAX_USERS_PER_ROOM, ADMIN_API_URL, SOCKET_IDLE_TIMER} from "../Enum/EnvironmentVariable"; export class IoSocketController { private nextUserId: number = 1; @@ -164,6 +164,7 @@ export class IoSocketController { let memberTags: string[] = []; let memberTextures: CharacterTexture[] = []; + const room = await socketManager.getOrCreateRoom(roomId); if(room.isFull){ throw new Error('Room is full'); @@ -240,7 +241,13 @@ export class IoSocketController { open: (ws) => { // Let's join the room const client = this.initClient(ws); //todo: into the upgrade instead? - socketManager.handleJoinRoom(client); + const room = socketManager.getRoomById(client.roomId); + + if (room && room.isFull) { + socketManager.emitCloseMessage(client, 302); + }else { + socketManager.handleJoinRoom(client); + } //get data information and show messages if (ADMIN_API_URL) { @@ -262,7 +269,14 @@ export class IoSocketController { } }, message: (ws, arrayBuffer, isBinary): void => { + const client = ws as ExSocketInterface; + + const room = socketManager.getRoomById(client.roomId); + if (room && room.isFull) { + return; + } + const message = ClientToServerMessage.deserializeBinary(new Uint8Array(arrayBuffer)); if (message.hasViewportmessage()) { @@ -286,7 +300,6 @@ export class IoSocketController { } else if (message.hasQueryjitsijwtmessage()){ socketManager.handleQueryJitsiJwtMessage(client, message.getQueryjitsijwtmessage() as QueryJitsiJwtMessage); } - /* Ok is false if backpressure was built up, wait for drain */ //let ok = ws.send(message, isBinary); }, diff --git a/back/src/Services/SocketManager.ts b/back/src/Services/SocketManager.ts index 97f008c4..0e0440b8 100644 --- a/back/src/Services/SocketManager.ts +++ b/back/src/Services/SocketManager.ts @@ -23,7 +23,9 @@ import { WebRtcStartMessage, QueryJitsiJwtMessage, SendJitsiJwtMessage, - SendUserMessage + CharacterLayerMessage, + SendUserMessage, + CloseMessage } from "../Messages/generated/messages_pb"; import {PointInterface} from "../Model/Websocket/PointInterface"; import {User} from "../Model/User"; @@ -57,7 +59,7 @@ export interface AdminSocketData { export class SocketManager { private Worlds: Map = new Map(); private sockets: Map = new Map(); - + constructor() { clientEventsEmitter.registerToClientJoin((clientUUid: string, roomId: string) => { gaugeManager.incNbClientPerRoomGauge(roomId); @@ -390,6 +392,10 @@ export class SocketManager { return Promise.resolve(world) } + getRoomById(roomId: string) { + return this.Worlds.get(roomId); + } + private joinRoom(client : ExSocketInterface, position: PointInterface): GameRoom { const roomId = client.roomId; @@ -677,6 +683,19 @@ export class SocketManager { return socket; } + public emitCloseMessage(socket: ExSocketInterface, status: number): ExSocketInterface { + const closeMessage = new CloseMessage(); + closeMessage.setStatus(status); + + const serverToClientMessage = new ServerToClientMessage(); + serverToClientMessage.setClosemessage(closeMessage); + + if (!socket.disconnecting) { + socket.send(serverToClientMessage.serializeBinary().buffer, true); + } + return socket; + } + /** * Merges the characterLayers received from the front (as an array of string) with the custom textures from the back. */ diff --git a/front/dist/.gitignore b/front/dist/.gitignore new file mode 100644 index 00000000..20b097e9 --- /dev/null +++ b/front/dist/.gitignore @@ -0,0 +1 @@ +tests/* \ No newline at end of file diff --git a/front/src/Connexion/ConnectionManager.ts b/front/src/Connexion/ConnectionManager.ts index 6f3aad04..7f3c1c41 100644 --- a/front/src/Connexion/ConnectionManager.ts +++ b/front/src/Connexion/ConnectionManager.ts @@ -9,6 +9,11 @@ import {Room} from "./Room"; const URL_ROOM_STARTED = '/Floor0/floor0.json'; +export enum connexionErrorTypes { + serverError = 1, + tooManyUsers, +} + class ConnectionManager { private localUser!:LocalUser; @@ -92,8 +97,11 @@ class ConnectionManager { return new Promise((resolve, reject) => { const connection = new RoomConnection(this.localUser.jwtToken, roomId, name, characterLayers, position, viewport); connection.onConnectError((error: object) => { - console.log('An error occurred while connecting to socket server. Retrying'); - reject(error); + if (error) { //todo: how to check error type? + reject(connexionErrorTypes.tooManyUsers); + } else { + reject(connexionErrorTypes.serverError); + } }); connection.onConnect(() => { resolve(connection); @@ -101,6 +109,8 @@ class ConnectionManager { }).catch((err) => { // Let's retry in 4-6 seconds return new Promise((resolve, reject) => { + if (err === connexionErrorTypes.tooManyUsers) return reject(err); + console.log('An error occurred while connecting to socket server. Retrying'); setTimeout(() => { //todo: allow a way to break recurrsion? this.connectToRoomSocket(roomId, name, characterLayers, position, viewport).then((connection) => resolve(connection)); diff --git a/front/src/Connexion/ConnexionModels.ts b/front/src/Connexion/ConnexionModels.ts index 63d87566..19fec57e 100644 --- a/front/src/Connexion/ConnexionModels.ts +++ b/front/src/Connexion/ConnexionModels.ts @@ -30,6 +30,8 @@ export enum EventMessage{ TELEPORT = "teleport", USER_MESSAGE = "user-message", START_JITSI_ROOM = "start-jitsi-room", + + CLOSE_MESSAGE = "close-message", } export interface PointInterface { diff --git a/front/src/Connexion/RoomConnection.ts b/front/src/Connexion/RoomConnection.ts index b25e2d76..578b8bff 100644 --- a/front/src/Connexion/RoomConnection.ts +++ b/front/src/Connexion/RoomConnection.ts @@ -26,6 +26,8 @@ import { QueryJitsiJwtMessage, SendJitsiJwtMessage, CharacterLayerMessage, + SendUserMessage, + CloseMessage, PingMessage, SendUserMessage } from "../Messages/generated/messages_pb" @@ -162,10 +164,11 @@ export class RoomConnection implements RoomConnection { this.dispatch(EventMessage.START_JITSI_ROOM, message.getSendjitsijwtmessage()); } else if (message.hasSendusermessage()) { this.dispatch(EventMessage.USER_MESSAGE, message.getSendusermessage()); + } else if (message.hasClosemessage()) { + this.dispatch(EventMessage.CLOSE_MESSAGE, message.getClosemessage()); } else { throw new Error('Unknown message received'); } - } } @@ -546,6 +549,12 @@ export class RoomConnection implements RoomConnection { }); } + public onCloseMessage(callback: (status: number) => void): void { + return this.onMessage(EventMessage.CLOSE_MESSAGE, (message: CloseMessage) => { + callback(message.getStatus()); + }); + } + public hasTag(tag: string): boolean { return this.tags.includes(tag); } diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index a704062e..7c6c475f 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -46,7 +46,7 @@ import {ActionableItem} from "../Items/ActionableItem"; import {UserInputManager} from "../UserInput/UserInputManager"; import {UserMovedMessage} from "../../Messages/generated/messages_pb"; import {ProtobufClientUtils} from "../../Network/ProtobufClientUtils"; -import {connectionManager} from "../../Connexion/ConnectionManager"; +import {connectionManager, connexionErrorTypes} from "../../Connexion/ConnectionManager"; import {RoomConnection} from "../../Connexion/RoomConnection"; import {GlobalMessageManager} from "../../Administration/GlobalMessageManager"; import {UserMessageManager} from "../../Administration/UserMessageManager"; @@ -54,6 +54,13 @@ import {ConsoleGlobalMessageManager} from "../../Administration/ConsoleGlobalMes import {ResizableScene} from "../Login/ResizableScene"; import {Room} from "../../Connexion/Room"; import {jitsiFactory} from "../../WebRtc/JitsiFactory"; +import {MessageUI} from "../../Logger/MessageUI"; +import {ErrorScene} from "../Reconnecting/ErrorScene"; + + +export enum Textures { + Player = "male1" +} export interface GameSceneInitInterface { initPosition: PointInterface|null @@ -117,6 +124,7 @@ export class GameScene extends ResizableScene implements CenterListener { // A promise that will resolve when the "create" method is called (signaling loading is ended) private createPromise: Promise; private createPromiseResolve!: (value?: void | PromiseLike) => void; + private offlineMode: boolean = false; MapKey: string; MapUrlFile: string; @@ -395,7 +403,7 @@ export class GameScene extends ResizableScene implements CenterListener { if (this.connection === undefined) { // Let's wait 0.5 seconds before printing the "connecting" screen to avoid blinking setTimeout(() => { - if (this.connection === undefined) { + if (this.connection === undefined && !this.offlineMode) { this.scene.sleep(); this.scene.launch(ReconnectingSceneName); } @@ -539,6 +547,46 @@ export class GameScene extends ResizableScene implements CenterListener { this.startJitsi(room, jwt); }); + connection.onCloseMessage((status: number) => { + this.connection.closeConnection(); + this.simplePeer.unregister(); + connection.closeConnection(); + + const waitGameSceneKey = 'somekey' + Math.round(Math.random() * 10000); + //show wait scene + setTimeout(() => { + /** + * Note: the ErrorScene could then become a singleton. In the future, + * an error message could originate from the server directly. + * **/ + const game: Phaser.Scene = new ErrorScene(waitGameSceneKey); + this.scene.add(waitGameSceneKey, game, true, { + initPosition: { + x: this.CurrentPlayer.x, + y: this.CurrentPlayer.y + } + }); + this.scene.stop(this.scene.key); + this.scene.start(waitGameSceneKey, { + status: status, + text : 'Oops! WorkAdventure is too popular, ' + + '\n' + + '\n' + + 'the maximum number of players has been reached!' + + '\n' + + '\n' + + `Reconnect in 30 secondes ...` + }); + }, 500); + + //trying to reload map + setTimeout(() => { + this.scene.stop(waitGameSceneKey); + this.scene.remove(waitGameSceneKey); + this.scene.start(this.scene.key); + }, 30000); + }); + // When connection is performed, let's connect SimplePeer this.simplePeer = new SimplePeer(this.connection, !this.room.isPublic, this.GameManager.getPlayerName()); this.GlobalMessageManager = new GlobalMessageManager(this.connection); @@ -572,6 +620,11 @@ export class GameScene extends ResizableScene implements CenterListener { this.gameMap.setPosition(this.CurrentPlayer.x, this.CurrentPlayer.y); return connection; + }).catch(error => { + if (error === connexionErrorTypes.tooManyUsers) { + this.offlineMode = true; + MessageUI.warningMessage('Too many users. You switched to offline mode. Please try to connect again later.'); + } }); } @@ -621,7 +674,7 @@ export class GameScene extends ResizableScene implements CenterListener { this.chatModeSprite.setFrame(3); } } - + private initStartXAndStartY() { // If there is an init position passed if (this.initPosition !== null) { diff --git a/front/src/Phaser/Reconnecting/ErrorScene.ts b/front/src/Phaser/Reconnecting/ErrorScene.ts new file mode 100644 index 00000000..53080c55 --- /dev/null +++ b/front/src/Phaser/Reconnecting/ErrorScene.ts @@ -0,0 +1,71 @@ +import {TextField} from "../Components/TextField"; +import Image = Phaser.GameObjects.Image; +import {ResizableScene} from "../Login/ResizableScene"; + +enum ReconnectingTextures { + icon = "icon", + mainFont = "main_font" +} + +export class ErrorScene extends ResizableScene { + private reconnectingField!: TextField; + private catImage!: Phaser.Physics.Arcade.Sprite; + private logo!: Image; + private text: string = ''; + private status: number = 404; + + constructor(key: string) { + super({ + key: key + }); + } + + init(data: {status: number, text: string}){ + this.text = data.text; + this .status = data.status; + } + + preload() { + this.load.image(ReconnectingTextures.icon, "resources/logos/tcm_full.png"); + // Note: arcade.png from the Phaser 3 examples at: https://github.com/photonstorm/phaser3-examples/tree/master/public/assets/fonts/bitmap + this.load.bitmapFont(ReconnectingTextures.mainFont, 'resources/fonts/arcade.png', 'resources/fonts/arcade.xml'); + this.load.spritesheet( + 'cat', + 'resources/characters/pipoya/Cat 01-1.png', + {frameWidth: 32, frameHeight: 32} + ); + } + + create() { + this.logo = new Image(this, this.game.renderer.width - 30, this.game.renderer.height - 20, ReconnectingTextures.icon); + this.add.existing(this.logo); + + this.reconnectingField = new TextField( + this, + this.game.renderer.width / 2, + this.game.renderer.height / 2, + this.text); + + this.catImage = this.physics.add.sprite( + this.game.renderer.width / 2, + this.game.renderer.height / 2 - 70, + 'cat'); + + this.anims.create({ + key: 'right', + frames: this.anims.generateFrameNumbers('cat', {start: 6, end: 8}), + frameRate: 10, + repeat: -1 + }); + this.catImage.play('right'); + } + + onResize(){ + this.reconnectingField.x = this.game.renderer.width / 2; + this.reconnectingField.y = this.game.renderer.height / 2; + this.catImage.x = this.game.renderer.width / 2; + this.catImage.y = this.game.renderer.height / 2 - 70; + this.logo.x = this.game.renderer.width - 30; + this.logo.y = this.game.renderer.height - 30; + } +} diff --git a/messages/messages.proto b/messages/messages.proto index 6e0b47df..5fb7be16 100644 --- a/messages/messages.proto +++ b/messages/messages.proto @@ -193,6 +193,10 @@ message SendUserMessage{ string message = 2; } +message CloseMessage{ + int32 status = 1; +} + message ServerToClientMessage { oneof message { BatchMessage batchMessage = 1; @@ -207,5 +211,6 @@ message ServerToClientMessage { TeleportMessageMessage teleportMessageMessage = 10; SendJitsiJwtMessage sendJitsiJwtMessage = 11; SendUserMessage sendUserMessage = 12; + CloseMessage closeMessage = 13; } }