diff --git a/back/src/RoomManager.ts b/back/src/RoomManager.ts index 256c904e..0a8e4a01 100644 --- a/back/src/RoomManager.ts +++ b/back/src/RoomManager.ts @@ -2,7 +2,7 @@ import {IRoomManagerServer} from "./Messages/generated/messages_grpc_pb"; import { AdminGlobalMessage, AdminMessage, - AdminPusherToBackMessage, + AdminPusherToBackMessage, AdminRoomMessage, BanMessage, EmptyMessage, @@ -15,7 +15,7 @@ import { ServerToClientMessage, SilentMessage, UserMovesMessage, - WebRtcSignalToServerMessage, + WebRtcSignalToServerMessage, WorldFullWarningToRoomMessage, ZoneMessage } from "./Messages/generated/messages_pb"; import {sendUnaryData, ServerDuplexStream, ServerUnaryCall, ServerWritableStream} from "grpc"; @@ -184,6 +184,10 @@ const roomManager: IRoomManagerServer = { socketManager.sendAdminRoomMessage(call.request.getRoomid(), call.request.getMessage()); callback(null, new EmptyMessage()); }, + sendWorldFullWarningToRoom(call: ServerUnaryCall, callback: sendUnaryData): void { + socketManager.dispatchWorlFullWarning(call.request.getRoomid()); + callback(null, new EmptyMessage()); + }, }; export {roomManager}; diff --git a/back/src/Services/SocketManager.ts b/back/src/Services/SocketManager.ts index 6317a46d..40023833 100644 --- a/back/src/Services/SocketManager.ts +++ b/back/src/Services/SocketManager.ts @@ -24,6 +24,7 @@ import { UserJoinedZoneMessage, GroupUpdateZoneMessage, GroupLeftZoneMessage, + WorldFullWarningMessage, UserLeftZoneMessage, BanUserMessage, } from "../Messages/generated/messages_pb"; @@ -58,6 +59,7 @@ function emitZoneMessage(subMessage: SubToPusherMessage, socket: ZoneSocket): vo // TODO: should we batch those every 100ms? const batchMessage = new BatchToPusherMessage(); batchMessage.addPayload(subMessage); + socket.write(batchMessage); } @@ -298,20 +300,14 @@ export class SocketManager { const roomId = joinRoomMessage.getRoomid(); - const world = await socketManager.getOrCreateRoom(roomId); - - // Dispatch groups position to newly connected user - /*world.getGroups().forEach((group: Group) => { - this.emitCreateUpdateGroupEvent(socket, group); - });*/ + const room = await socketManager.getOrCreateRoom(roomId); //join world - const user = world.join(socket, joinRoomMessage); + const user = room.join(socket, joinRoomMessage); clientEventsEmitter.emitClientJoin(user.uuid, roomId); - //console.log(new Date().toISOString() + ' A user joined (', this.sockets.size, ' connected users)'); console.log(new Date().toISOString() + ' A user joined'); - return {room: world, user}; + return {room, user}; } private onZoneEnter(thing: Movable, fromZone: Zone|null, listener: ZoneSocket) { @@ -758,6 +754,24 @@ export class SocketManager { recipient.socket.write(clientMessage); }); } + + dispatchWorlFullWarning(roomId: string,): void { + const room = this.rooms.get(roomId); + if (!room) { + //todo: this should cause the http call to return a 500 + console.error("In sendAdminRoomMessage, could not find room with id '" + roomId + "'. Maybe the room was closed a few milliseconds ago and there was a race condition?"); + return; + } + + room.getUsers().forEach((recipient) => { + const worldFullMessage = new WorldFullWarningMessage(); + + const clientMessage = new ServerToClientMessage(); + clientMessage.setWorldfullwarningmessage(worldFullMessage); + + recipient.socket.write(clientMessage); + }); + } } export const socketManager = new SocketManager(); diff --git a/front/dist/resources/html/warningContainer.html b/front/dist/resources/html/warningContainer.html new file mode 100644 index 00000000..832ac4da --- /dev/null +++ b/front/dist/resources/html/warningContainer.html @@ -0,0 +1,18 @@ + + +
+

Warning!

+

This world is close to its limit!

+
\ No newline at end of file diff --git a/front/src/Connexion/ConnectionManager.ts b/front/src/Connexion/ConnectionManager.ts index 0b6e9ff1..522407e3 100644 --- a/front/src/Connexion/ConnectionManager.ts +++ b/front/src/Connexion/ConnectionManager.ts @@ -7,21 +7,15 @@ import {localUserStore} from "./LocalUserStore"; import {LocalUser} from "./LocalUser"; import {Room} from "./Room"; import {Subject} from "rxjs"; +import {ServerToClientMessage} from "../Messages/generated/messages_pb"; -export enum ConnexionMessageEventTypes { - worldFull = 1, -} - -export interface ConnexionMessageEvent { - type: ConnexionMessageEventTypes, -} class ConnectionManager { private localUser!:LocalUser; private connexionType?: GameConnexionTypes - public _connexionMessageStream:Subject = new Subject(); + public _serverToClientMessageStream:Subject = new Subject(); /** * Tries to login to the node server and return the starting map url to be loaded */ diff --git a/front/src/Connexion/RoomConnection.ts b/front/src/Connexion/RoomConnection.ts index 5951d078..debb99f4 100644 --- a/front/src/Connexion/RoomConnection.ts +++ b/front/src/Connexion/RoomConnection.ts @@ -43,7 +43,8 @@ import { } from "./ConnexionModels"; import {BodyResourceDescriptionInterface} from "../Phaser/Entity/PlayerTextures"; import {adminMessagesService} from "./AdminMessagesService"; -import {connectionManager, ConnexionMessageEventTypes} from "./ConnectionManager"; +import {worldFullMessageStream} from "./WorldFullMessageStream"; +import {worldFullWarningStream} from "./WorldFullWarningStream"; const manualPingDelay = 20000; @@ -156,8 +157,8 @@ export class RoomConnection implements RoomConnection { items } as RoomJoinedMessageInterface }); - } else if (message.hasErrormessage()) { - connectionManager._connexionMessageStream.next({type: ConnexionMessageEventTypes.worldFull}); //todo: generalize this behavior to all messages + } else if (message.hasWorldfullmessage()) { + worldFullMessageStream.onMessage(); this.closed = true; } else if (message.hasWebrtcsignaltoclientmessage()) { this.dispatch(EventMessage.WEBRTC_SIGNAL, message.getWebrtcsignaltoclientmessage()); @@ -179,6 +180,8 @@ export class RoomConnection implements RoomConnection { adminMessagesService.onSendusermessage(message.getSendusermessage() as SendUserMessage); } else if (message.hasBanusermessage()) { adminMessagesService.onSendusermessage(message.getSendusermessage() as BanUserMessage); + } else if (message.hasWorldfullwarningmessage()) { + worldFullWarningStream.onMessage(); } else { throw new Error('Unknown message received'); } diff --git a/front/src/Connexion/WorldFullMessageStream.ts b/front/src/Connexion/WorldFullMessageStream.ts new file mode 100644 index 00000000..c02530fa --- /dev/null +++ b/front/src/Connexion/WorldFullMessageStream.ts @@ -0,0 +1,14 @@ +import {Subject} from "rxjs"; + +class WorldFullMessageStream { + + private _stream:Subject = new Subject(); + public stream = this._stream.asObservable(); + + + onMessage() { + this._stream.next(); + } +} + +export const worldFullMessageStream = new WorldFullMessageStream(); \ No newline at end of file diff --git a/front/src/Connexion/WorldFullWarningStream.ts b/front/src/Connexion/WorldFullWarningStream.ts new file mode 100644 index 00000000..5e552830 --- /dev/null +++ b/front/src/Connexion/WorldFullWarningStream.ts @@ -0,0 +1,14 @@ +import {Subject} from "rxjs"; + +class WorldFullWarningStream { + + private _stream:Subject = new Subject(); + public stream = this._stream.asObservable(); + + + onMessage() { + this._stream.next(); + } +} + +export const worldFullWarningStream = new WorldFullWarningStream(); \ No newline at end of file diff --git a/front/src/Phaser/Components/WarningContainer.ts b/front/src/Phaser/Components/WarningContainer.ts new file mode 100644 index 00000000..97e97660 --- /dev/null +++ b/front/src/Phaser/Components/WarningContainer.ts @@ -0,0 +1,14 @@ + +export const warningContainerKey = 'warningContainer'; +export const warningContainerHtml = 'resources/html/warningContainer.html'; + +export class WarningContainer extends Phaser.GameObjects.DOMElement { + + constructor(scene: Phaser.Scene) { + super(scene, 100, 0); + this.setOrigin(0, 0); + this.createFromCache(warningContainerKey); + this.scene.add.existing(this); + } + +} \ No newline at end of file diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 9321dbce..15872658 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -41,7 +41,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, ConnexionMessageEvent, ConnexionMessageEventTypes} from "../../Connexion/ConnectionManager"; +import {connectionManager} from "../../Connexion/ConnectionManager"; import {RoomConnection} from "../../Connexion/RoomConnection"; import {GlobalMessageManager} from "../../Administration/GlobalMessageManager"; import {userMessageManager} from "../../Administration/UserMessageManager"; @@ -65,6 +65,7 @@ import CanvasTexture = Phaser.Textures.CanvasTexture; import GameObject = Phaser.GameObjects.GameObject; import FILE_LOAD_ERROR = Phaser.Loader.Events.FILE_LOAD_ERROR; import {Subscription} from "rxjs"; +import {worldFullMessageStream} from "../../Connexion/WorldFullMessageStream"; export interface GameSceneInitInterface { initPosition: PointInterface|null, @@ -306,7 +307,7 @@ export class GameScene extends ResizableScene implements CenterListener { urlManager.pushRoomIdToUrl(this.room); this.startLayerName = urlManager.getStartLayerNameFromUrl(); - this.messageSubscription = connectionManager._connexionMessageStream.subscribe((event) => this.onConnexionMessage(event)) + this.messageSubscription = worldFullMessageStream.stream.subscribe((message) => this.showWorldFullError()) const playerName = gameManager.getPlayerName(); if (!playerName) { @@ -1230,7 +1231,7 @@ export class GameScene extends ResizableScene implements CenterListener { mediaManager.removeTriggerCloseJitsiFrameButton('close-jisi'); } - //todo: into onConnexionMessage + //todo: put this into an 'orchestrator' scene (EntryScene?) private bannedUser(){ this.cleanupClosingScene(); this.userInputManager.clearAllKeys(); @@ -1241,16 +1242,15 @@ export class GameScene extends ResizableScene implements CenterListener { }); } - private onConnexionMessage(event: ConnexionMessageEvent) { - if (event.type === ConnexionMessageEventTypes.worldFull) { - this.cleanupClosingScene(); - this.scene.stop(ReconnectingSceneName); - this.userInputManager.clearAllKeys(); - this.scene.start(ErrorSceneName, { - title: 'Connection rejected', - subTitle: 'The world you are trying to join is full. Try again later.', - message: 'If you want more information, you may contact us at: workadventure@thecodingmachine.com' - }); - } + //todo: put this into an 'orchestrator' scene (EntryScene?) + private showWorldFullError(): void { + this.cleanupClosingScene(); + this.scene.stop(ReconnectingSceneName); + this.userInputManager.clearAllKeys(); + this.scene.start(ErrorSceneName, { + title: 'Connection rejected', + subTitle: 'The world you are trying to join is full. Try again later.', + message: 'If you want more information, you may contact us at: workadventure@thecodingmachine.com' + }); } } diff --git a/front/src/Phaser/Login/EnableCameraScene.ts b/front/src/Phaser/Login/EnableCameraScene.ts index 515e1852..a3ca5cf1 100644 --- a/front/src/Phaser/Login/EnableCameraScene.ts +++ b/front/src/Phaser/Login/EnableCameraScene.ts @@ -61,26 +61,22 @@ export class EnableCameraScene extends Phaser.Scene { this.microphoneNameField = new TextField(this, this.game.renderer.width / 2, this.game.renderer.height - 40, ''); this.arrowRight = new Image(this, 0, 0, LoginTextures.arrowRight); - this.arrowRight.setOrigin(0.5, 0.5); this.arrowRight.setVisible(false); this.arrowRight.setInteractive().on('pointerdown', this.nextCam.bind(this)); this.add.existing(this.arrowRight); this.arrowLeft = new Image(this, 0, 0, LoginTextures.arrowRight); - this.arrowLeft.setOrigin(0.5, 0.5); this.arrowLeft.setVisible(false); this.arrowLeft.flipX = true; this.arrowLeft.setInteractive().on('pointerdown', this.previousCam.bind(this)); this.add.existing(this.arrowLeft); this.arrowUp = new Image(this, 0, 0, LoginTextures.arrowUp); - this.arrowUp.setOrigin(0.5, 0.5); this.arrowUp.setVisible(false); this.arrowUp.setInteractive().on('pointerdown', this.previousMic.bind(this)); this.add.existing(this.arrowUp); this.arrowDown = new Image(this, 0, 0, LoginTextures.arrowUp); - this.arrowDown.setOrigin(0.5, 0.5); this.arrowDown.setVisible(false); this.arrowDown.flipY = true; this.arrowDown.setInteractive().on('pointerdown', this.nextMic.bind(this)); @@ -164,8 +160,6 @@ export class EnableCameraScene extends Phaser.Scene { private updateWebCamName(): void { if (this.camerasList.length > 1) { - const div = HtmlUtils.getElementByIdOrFail('myCamVideoSetup'); - let label = this.camerasList[this.cameraSelected].label; // remove text in parenthesis label = label.replace(/\([^()]*\)/g, '').trim(); @@ -173,17 +167,8 @@ export class EnableCameraScene extends Phaser.Scene { label = label.normalize("NFD").replace(/[\u0300-\u036f]/g, ""); this.cameraNameField.text = label; - if (this.cameraSelected < this.camerasList.length - 1) { - this.arrowRight.setVisible(true); - } else { - this.arrowRight.setVisible(false); - } - - if (this.cameraSelected > 0) { - this.arrowLeft.setVisible(true); - } else { - this.arrowLeft.setVisible(false); - } + this.arrowRight.setVisible(this.cameraSelected < this.camerasList.length - 1); + this.arrowLeft.setVisible(this.cameraSelected > 0); } if (this.microphonesList.length > 1) { let label = this.microphonesList[this.microphoneSelected].label; @@ -194,17 +179,8 @@ export class EnableCameraScene extends Phaser.Scene { this.microphoneNameField.text = label; - if (this.microphoneSelected < this.microphonesList.length - 1) { - this.arrowDown.setVisible(true); - } else { - this.arrowDown.setVisible(false); - } - - if (this.microphoneSelected > 0) { - this.arrowUp.setVisible(true); - } else { - this.arrowUp.setVisible(false); - } + this.arrowDown.setVisible(this.microphoneSelected < this.microphonesList.length - 1); + this.arrowUp.setVisible(this.microphoneSelected > 0); } this.reposition(); diff --git a/front/src/Phaser/Menu/MenuScene.ts b/front/src/Phaser/Menu/MenuScene.ts index ab25c338..6ed26730 100644 --- a/front/src/Phaser/Menu/MenuScene.ts +++ b/front/src/Phaser/Menu/MenuScene.ts @@ -6,6 +6,8 @@ import {mediaManager} from "../../WebRtc/MediaManager"; import {gameReportKey, gameReportRessource, ReportMenu} from "./ReportMenu"; import {connectionManager} from "../../Connexion/ConnectionManager"; import {GameConnexionTypes} from "../../Url/UrlManager"; +import {WarningContainer, warningContainerHtml, warningContainerKey} from "../Components/WarningContainer"; +import {worldFullWarningStream} from "../../Connexion/WorldFullWarningStream"; export const MenuSceneName = 'MenuScene'; const gameMenuKey = 'gameMenu'; @@ -30,6 +32,8 @@ export class MenuScene extends Phaser.Scene { private gameQualityValue: number; private videoQualityValue: number; private menuButton!: Phaser.GameObjects.DOMElement; + private warningContainer: WarningContainer | null = null; + private warningContainerTimeout: NodeJS.Timeout | null = null; constructor() { super({key: MenuSceneName}); @@ -44,6 +48,7 @@ export class MenuScene extends Phaser.Scene { this.load.html(gameSettingsMenuKey, 'resources/html/gameQualityMenu.html'); this.load.html(gameShare, 'resources/html/gameShare.html'); this.load.html(gameReportKey, gameReportRessource); + this.load.html(warningContainerKey, warningContainerHtml); } create() { @@ -85,6 +90,8 @@ export class MenuScene extends Phaser.Scene { this.menuElement.addListener('click'); this.menuElement.on('click', this.onMenuClick.bind(this)); + + worldFullWarningStream.stream.subscribe(() => this.showWorldCapacityWarning()); } //todo put this method in a parent menuElement class @@ -121,6 +128,21 @@ export class MenuScene extends Phaser.Scene { ease: 'Power3' }); } + + private showWorldCapacityWarning() { + if (!this.warningContainer) { + this.warningContainer = new WarningContainer(this); + } + if (this.warningContainerTimeout) { + clearTimeout(this.warningContainerTimeout); + } + this.warningContainerTimeout = setTimeout(() => { + this.warningContainer?.destroy(); + this.warningContainer = null + this.warningContainerTimeout = null + }, 2000); + + } private closeSideMenu(): void { if (!this.sideMenuOpened) return; diff --git a/messages/protos/messages.proto b/messages/protos/messages.proto index 6c747768..cc23ed24 100644 --- a/messages/protos/messages.proto +++ b/messages/protos/messages.proto @@ -197,6 +197,15 @@ message SendUserMessage{ string message = 2; } +message WorldFullWarningMessage{ +} +message WorldFullWarningToRoomMessage{ + string roomId = 1; +} + +message WorldFullMessage{ +} + message BanUserMessage{ string type = 1; string message = 2; @@ -218,6 +227,8 @@ message ServerToClientMessage { SendUserMessage sendUserMessage = 12; BanUserMessage banUserMessage = 13; AdminRoomMessage adminRoomMessage = 14; + WorldFullWarningMessage worldFullWarningMessage = 15; + WorldFullMessage worldFullMessage = 16; } } @@ -383,4 +394,5 @@ service RoomManager { rpc sendGlobalAdminMessage(AdminGlobalMessage) returns (EmptyMessage); rpc ban(BanMessage) returns (EmptyMessage); rpc sendAdminMessageToRoom(AdminRoomMessage) returns (EmptyMessage); + rpc sendWorldFullWarningToRoom(WorldFullWarningToRoomMessage) returns (EmptyMessage); } diff --git a/pusher/src/Controller/AdminController.ts b/pusher/src/Controller/AdminController.ts index 8c4f524d..69123743 100644 --- a/pusher/src/Controller/AdminController.ts +++ b/pusher/src/Controller/AdminController.ts @@ -2,7 +2,7 @@ import {BaseController} from "./BaseController"; import {HttpRequest, HttpResponse, TemplatedApp} from "uWebSockets.js"; import {ADMIN_API_TOKEN} from "../Enum/EnvironmentVariable"; import {apiClientRepository} from "../Services/ApiClientRepository"; -import {AdminRoomMessage} from "../Messages/generated/messages_pb"; +import {AdminRoomMessage, WorldFullWarningToRoomMessage} from "../Messages/generated/messages_pb"; export class AdminController extends BaseController{ @@ -40,22 +40,36 @@ export class AdminController extends BaseController{ if (typeof body.text !== 'string') { throw 'Incorrect text parameter' } + if (body.type !== 'warning' || body.type !== 'message') { + throw 'Incorrect type parameter' + } if (!body.targets || typeof body.targets !== 'object') { throw 'Incorrect targets parameter' } const text: string = body.text; + const type: string = body.type; const targets: string[] = body.targets; await Promise.all(targets.map((roomId) => { return apiClientRepository.getClient(roomId).then((roomClient) =>{ return new Promise((res, rej) => { - const roomMessage = new AdminRoomMessage(); - roomMessage.setMessage(text); - roomMessage.setRoomid(roomId); - - roomClient.sendAdminMessageToRoom(roomMessage, (err) => { - err ? rej(err) : res(); - }); + if (type === 'message') { + const roomMessage = new AdminRoomMessage(); + roomMessage.setMessage(text); + roomMessage.setRoomid(roomId); + + roomClient.sendAdminMessageToRoom(roomMessage, (err) => { + err ? rej(err) : res(); + }); + } else { + const roomMessage = new WorldFullWarningToRoomMessage(); + roomMessage.setRoomid(roomId); + + roomClient.sendWorldFullWarningToRoom(roomMessage, (err) => { + err ? rej(err) : res(); + }); + } + }); }); })); diff --git a/pusher/src/Controller/IoSocketController.ts b/pusher/src/Controller/IoSocketController.ts index 82d0707a..85a80e11 100644 --- a/pusher/src/Controller/IoSocketController.ts +++ b/pusher/src/Controller/IoSocketController.ts @@ -258,12 +258,12 @@ export class IoSocketController { /* Handlers */ open: (ws) => { if(ws.rejected === true) { - emitError(ws, 'World is full'); + socketManager.emitWorldFullMessage(ws); ws.close(); } // Let's join the room - const client = this.initClient(ws); //todo: into the upgrade instead? + const client = this.initClient(ws); socketManager.handleJoinRoom(client); //get data information and show messages diff --git a/pusher/src/Services/SocketManager.ts b/pusher/src/Services/SocketManager.ts index 7d5a063e..9b698e38 100644 --- a/pusher/src/Services/SocketManager.ts +++ b/pusher/src/Services/SocketManager.ts @@ -19,15 +19,15 @@ import { JoinRoomMessage, CharacterLayerMessage, PusherToBackMessage, + WorldFullMessage, AdminPusherToBackMessage, ServerToAdminClientMessage, - SendUserMessage, - BanUserMessage, UserJoinedRoomMessage, UserLeftRoomMessage, AdminMessage, BanMessage + UserJoinedRoomMessage, UserLeftRoomMessage, AdminMessage, BanMessage } from "../Messages/generated/messages_pb"; import {ProtobufUtils} from "../Model/Websocket/ProtobufUtils"; import {JITSI_ISS, SECRET_JITSI_KEY} from "../Enum/EnvironmentVariable"; import {adminApi, CharacterTexture} from "./AdminApi"; -import {emitError, emitInBatch} from "./IoSocketHelpers"; +import {emitInBatch} from "./IoSocketHelpers"; import Jwt from "jsonwebtoken"; import {JITSI_URL} from "../Enum/EnvironmentVariable"; import {clientEventsEmitter} from "./ClientEventsEmitter"; @@ -36,6 +36,7 @@ import {apiClientRepository} from "./ApiClientRepository"; import {GroupDescriptor, UserDescriptor, ZoneEventListener} from "_Model/Zone"; import Debug from "debug"; import {ExAdminSocketInterface} from "_Model/Websocket/ExAdminSocketInterface"; +import {WebSocket} from "uWebSockets.js"; const debug = Debug('socket'); @@ -52,6 +53,7 @@ export interface AdminSocketData { } export class SocketManager implements ZoneEventListener { + private Worlds: Map = new Map(); private sockets: Map = new Map(); @@ -533,6 +535,15 @@ export class SocketManager implements ZoneEventListener { emitInBatch(listener, subMessage); } + + public emitWorldFullMessage(client: WebSocket) { + const errorMessage = new WorldFullMessage(); + + const serverToClientMessage = new ServerToClientMessage(); + serverToClientMessage.setWorldfullmessage(errorMessage); + + client.send(serverToClientMessage.serializeBinary().buffer, true); + } } export const socketManager = new SocketManager();