Browse Source

FEATURE: warning message when world is near full capacity

develop
kharhamel 2 months ago
parent
commit
8d6c1a50bf
  1. 8
      back/src/RoomManager.ts
  2. 32
      back/src/Services/SocketManager.ts
  3. 18
      front/dist/resources/html/warningContainer.html
  4. 10
      front/src/Connexion/ConnectionManager.ts
  5. 9
      front/src/Connexion/RoomConnection.ts
  6. 14
      front/src/Connexion/WorldFullMessageStream.ts
  7. 14
      front/src/Connexion/WorldFullWarningStream.ts
  8. 14
      front/src/Phaser/Components/WarningContainer.ts
  9. 28
      front/src/Phaser/Game/GameScene.ts
  10. 32
      front/src/Phaser/Login/EnableCameraScene.ts
  11. 22
      front/src/Phaser/Menu/MenuScene.ts
  12. 12
      messages/protos/messages.proto
  13. 30
      pusher/src/Controller/AdminController.ts
  14. 4
      pusher/src/Controller/IoSocketController.ts
  15. 17
      pusher/src/Services/SocketManager.ts

8
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<WorldFullWarningToRoomMessage>, callback: sendUnaryData<EmptyMessage>): void {
socketManager.dispatchWorlFullWarning(call.request.getRoomid());
callback(null, new EmptyMessage());
},
};
export {roomManager};

32
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();

18
front/dist/resources/html/warningContainer.html

@ -0,0 +1,18 @@
<style>
#warningMain {
border-radius: 5px;
height: 100px;
width: 300px;
background-color: red;
text-align: center;
}
#warningMain h2 {
padding: 5px;
}
</style>
<main id="warningMain">
<h2>Warning!</h2>
<p>This world is close to its limit!</p>
</main>

10
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<ConnexionMessageEvent> = new Subject();
public _serverToClientMessageStream:Subject<ServerToClientMessage> = new Subject();
/**
* Tries to login to the node server and return the starting map url to be loaded
*/

9
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');
}

14
front/src/Connexion/WorldFullMessageStream.ts

@ -0,0 +1,14 @@
import {Subject} from "rxjs";
class WorldFullMessageStream {
private _stream:Subject<void> = new Subject();
public stream = this._stream.asObservable();
onMessage() {
this._stream.next();
}
}
export const worldFullMessageStream = new WorldFullMessageStream();

14
front/src/Connexion/WorldFullWarningStream.ts

@ -0,0 +1,14 @@
import {Subject} from "rxjs";
class WorldFullWarningStream {
private _stream:Subject<void> = new Subject();
public stream = this._stream.asObservable();
onMessage() {
this._stream.next();
}
}
export const worldFullWarningStream = new WorldFullWarningStream();

14
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);
}
}

28
front/src/Phaser/Game/GameScene.ts

@ -39,7 +39,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";
@ -63,6 +63,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,
@ -304,7 +305,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) {
@ -1227,7 +1228,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();
@ -1238,16 +1239,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'
});
}
}

32
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<HTMLVideoElement>('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();

22
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;

12
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);
}

30
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();
});
}
});
});
}));

4
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

17
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<string, PusherRoom> = new Map<string, PusherRoom>();
private sockets: Map<number, ExSocketInterface> = new Map<number, ExSocketInterface>();
@ -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();
Loading…
Cancel
Save