From 65cefb35845ebc340b73f49806df468ae3058b05 Mon Sep 17 00:00:00 2001 From: jonny Date: Thu, 1 Jul 2021 15:50:40 +0200 Subject: [PATCH 001/101] fixed invalid unauathorized handler --- back/src/Controller/DebugController.ts | 2 +- pusher/src/Controller/DebugController.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/back/src/Controller/DebugController.ts b/back/src/Controller/DebugController.ts index b7f037fd..88287753 100644 --- a/back/src/Controller/DebugController.ts +++ b/back/src/Controller/DebugController.ts @@ -15,7 +15,7 @@ export class DebugController { const query = parse(req.getQuery()); if (query.token !== ADMIN_API_TOKEN) { - return res.status(401).send("Invalid token sent!"); + return res.writeStatus("401 Unauthorized").end("Invalid token sent!"); } return res diff --git a/pusher/src/Controller/DebugController.ts b/pusher/src/Controller/DebugController.ts index 0b0d188b..e9e3540d 100644 --- a/pusher/src/Controller/DebugController.ts +++ b/pusher/src/Controller/DebugController.ts @@ -16,7 +16,7 @@ export class DebugController { const query = parse(req.getQuery()); if (query.token !== ADMIN_API_TOKEN) { - return res.status(401).send("Invalid token sent!"); + return res.writeStatus("401 Unauthorized").end("Invalid token sent!"); } return res From 1806ef9d7e012d6a7ad6350d14ef9f5ddcf39674 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Mon, 28 Jun 2021 09:22:51 +0200 Subject: [PATCH 002/101] First version of the room metadata doc --- docs/maps/api-room.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/docs/maps/api-room.md b/docs/maps/api-room.md index 9d08ce1b..b8a99a53 100644 --- a/docs/maps/api-room.md +++ b/docs/maps/api-room.md @@ -144,3 +144,34 @@ WA.room.setTiles([ {x: 9, y: 4, tile: 'blue', layer: 'setTiles'} ]); ``` + +### Saving / loading metadata + +``` +WA.room.saveMetadata(key : string, data : any): void +WA.room.loadMetadata(key : string) : any +``` + +These 2 methods can be used to save and load data related to the current room. + +`data` can be any value that is serializable in JSON. + +Please refrain from storing large amounts of data in a room. Those functions are typically useful for saving or restoring +configuration / metadatas. + +Example : +```javascript +WA.room.saveMetadata('config', { + 'bottomExitUrl': '/@/org/world/castle', + 'topExitUrl': '/@/org/world/tower', + 'enableBirdSound': true +}); +//... +let config = WA.room.loadMetadata('config'); +``` + +{.alert.alert-danger} +Important: metadata can only be saved/loaded if an administration server is attached to WorkAdventure. The `WA.room.saveMetadata` +and `WA.room.loadMetadata` functions will therefore be available on the hosted version of WorkAdventure, but will not +be available in the self-hosted version (unless you decide to code an administration server stub to provide storage for +those data) From ea1460abaf341f7bbb5c9e86051c144c752387b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Fri, 2 Jul 2021 11:31:44 +0200 Subject: [PATCH 003/101] Adding variables (on the front side for now) --- docs/maps/api-room.md | 68 +++++++-- front/src/Api/Events/IframeEvent.ts | 8 ++ front/src/Api/Events/InitEvent.ts | 10 ++ front/src/Api/Events/SetVariableEvent.ts | 18 +++ front/src/Api/IframeListener.ts | 136 +++++++++++------- front/src/Api/iframe/room.ts | 47 +++++- front/src/Phaser/Game/GameScene.ts | 26 +++- .../src/Phaser/Game/SharedVariablesManager.ts | 59 ++++++++ front/src/iframe_api.ts | 6 + maps/tests/Variables/script.js | 11 ++ maps/tests/Variables/variables.json | 112 +++++++++++++++ maps/tests/index.html | 8 ++ messages/protos/messages.proto | 12 +- 13 files changed, 453 insertions(+), 68 deletions(-) create mode 100644 front/src/Api/Events/InitEvent.ts create mode 100644 front/src/Api/Events/SetVariableEvent.ts create mode 100644 front/src/Phaser/Game/SharedVariablesManager.ts create mode 100644 maps/tests/Variables/script.js create mode 100644 maps/tests/Variables/variables.json diff --git a/docs/maps/api-room.md b/docs/maps/api-room.md index b8a99a53..a307b2da 100644 --- a/docs/maps/api-room.md +++ b/docs/maps/api-room.md @@ -145,14 +145,15 @@ WA.room.setTiles([ ]); ``` -### Saving / loading metadata +### Saving / loading state ``` -WA.room.saveMetadata(key : string, data : any): void -WA.room.loadMetadata(key : string) : any +WA.room.saveVariable(key : string, data : unknown): void +WA.room.loadVariable(key : string) : unknown +WA.room.onVariableChange(key : string).subscribe((data: unknown) => {}) : Subscription ``` -These 2 methods can be used to save and load data related to the current room. +These 3 methods can be used to save, load and track changes in variables related to the current room. `data` can be any value that is serializable in JSON. @@ -161,17 +162,62 @@ configuration / metadatas. Example : ```javascript -WA.room.saveMetadata('config', { +WA.room.saveVariable('config', { 'bottomExitUrl': '/@/org/world/castle', 'topExitUrl': '/@/org/world/tower', 'enableBirdSound': true }); //... -let config = WA.room.loadMetadata('config'); +let config = WA.room.loadVariable('config'); ``` -{.alert.alert-danger} -Important: metadata can only be saved/loaded if an administration server is attached to WorkAdventure. The `WA.room.saveMetadata` -and `WA.room.loadMetadata` functions will therefore be available on the hosted version of WorkAdventure, but will not -be available in the self-hosted version (unless you decide to code an administration server stub to provide storage for -those data) +If you are using Typescript, please note that the return type of `loadVariable` is `unknown`. This is +for security purpose, as we don't know the type of the variable. In order to use the returned value, +you will need to cast it to the correct type (or better, use a [Type guard](https://www.typescriptlang.org/docs/handbook/2/narrowing.html) to actually check at runtime +that you get the expected type). + +{.alert.alert-warning} +For security reasons, you cannot load or save **any** variable (otherwise, anyone on your map could set any data). +Variables storage is subject to an authorization process. Read below to learn more. + +#### Declaring allowed keys + +In order to declare allowed keys related to a room, you need to add a **objects** in an "object layer" of the map. + +Each object will represent a variable. + +
+
+ +
+
+ +TODO: move the image in https://workadventu.re/img/docs + + +The name of the variable is the name of the object. +The object **type** MUST be **variable**. + +You can set a default value for the object in the `default` property. + +Use the `persist` property to save the state of the variable in database. If `persist` is false, the variable will stay +in the memory of the WorkAdventure servers but will be wiped out of the memory as soon as the room is empty (or if the +server restarts). + +{.alert.alert-info} +Do not use `persist` for highly dynamic values that have a short life spawn. + +With `readableBy` and `writableBy`, you control who can read of write in this variable. The property accepts a string +representing a "tag". Anyone having this "tag" can read/write in the variable. + +{.alert.alert-warning} +`readableBy` and `writableBy` are specific to the public version of WorkAdventure because the notion of tags +is not available unless you have an "admin" server (that is not part of the self-hosted version of WorkAdventure). + +Finally, the `jsonSchema` property can contain [a complete JSON schema](https://json-schema.org/) to validate the content of the variable. +Trying to set a variable to a value that is not compatible with the schema will fail. + + + + +TODO: document tracking, unsubscriber, etc... diff --git a/front/src/Api/Events/IframeEvent.ts b/front/src/Api/Events/IframeEvent.ts index fc3384f8..83d0e12e 100644 --- a/front/src/Api/Events/IframeEvent.ts +++ b/front/src/Api/Events/IframeEvent.ts @@ -18,6 +18,9 @@ import type { MenuItemClickedEvent } from "./ui/MenuItemClickedEvent"; import type { MenuItemRegisterEvent } from "./ui/MenuItemRegisterEvent"; import type { HasPlayerMovedEvent } from "./HasPlayerMovedEvent"; import type { SetTilesEvent } from "./SetTilesEvent"; +import type { SetVariableEvent } from "./SetVariableEvent"; +import type {InitEvent} from "./InitEvent"; + export interface TypedMessageEvent extends MessageEvent { data: T; @@ -50,6 +53,9 @@ export type IframeEventMap = { getState: undefined; registerMenuCommand: MenuItemRegisterEvent; setTiles: SetTilesEvent; + setVariable: SetVariableEvent; + // A script/iframe is ready to receive events + ready: null; }; export interface IframeEvent { type: T; @@ -68,6 +74,8 @@ export interface IframeResponseEventMap { hasPlayerMoved: HasPlayerMovedEvent; dataLayer: DataLayerEvent; menuItemClicked: MenuItemClickedEvent; + setVariable: SetVariableEvent; + init: InitEvent; } export interface IframeResponseEvent { type: T; diff --git a/front/src/Api/Events/InitEvent.ts b/front/src/Api/Events/InitEvent.ts new file mode 100644 index 00000000..47326f81 --- /dev/null +++ b/front/src/Api/Events/InitEvent.ts @@ -0,0 +1,10 @@ +import * as tg from "generic-type-guard"; + +export const isInitEvent = + new tg.IsInterface().withProperties({ + variables: tg.isObject + }).get(); +/** + * A message sent from the game just after an iFrame opens, to send all important data (like variables) + */ +export type InitEvent = tg.GuardedType; diff --git a/front/src/Api/Events/SetVariableEvent.ts b/front/src/Api/Events/SetVariableEvent.ts new file mode 100644 index 00000000..b0effb30 --- /dev/null +++ b/front/src/Api/Events/SetVariableEvent.ts @@ -0,0 +1,18 @@ +import * as tg from "generic-type-guard"; +import {isMenuItemRegisterEvent} from "./ui/MenuItemRegisterEvent"; + +export const isSetVariableEvent = + new tg.IsInterface().withProperties({ + key: tg.isString, + value: tg.isUnknown, + }).get(); +/** + * A message sent from the iFrame to the game to change the value of the property of the layer + */ +export type SetVariableEvent = tg.GuardedType; + +export const isSetVariableIframeEvent = + new tg.IsInterface().withProperties({ + type: tg.isSingletonString("setVariable"), + data: isSetVariableEvent + }).get(); diff --git a/front/src/Api/IframeListener.ts b/front/src/Api/IframeListener.ts index 314d5d2e..6caecc1f 100644 --- a/front/src/Api/IframeListener.ts +++ b/front/src/Api/IframeListener.ts @@ -32,6 +32,7 @@ import type { HasPlayerMovedEvent } from "./Events/HasPlayerMovedEvent"; import { isLoadPageEvent } from "./Events/LoadPageEvent"; import { handleMenuItemRegistrationEvent, isMenuItemRegisterIframeEvent } from "./Events/ui/MenuItemRegisterEvent"; import { SetTilesEvent, isSetTilesEvent } from "./Events/SetTilesEvent"; +import { isSetVariableIframeEvent, SetVariableEvent } from "./Events/SetVariableEvent"; type AnswererCallback = (query: IframeQueryMap[T]['query']) => IframeQueryMap[T]['answer']|Promise; @@ -40,6 +41,9 @@ type AnswererCallback = (query: IframeQueryMap[T * Also allows to send messages to those iframes. */ class IframeListener { + private readonly _readyStream: Subject = new Subject(); + public readonly readyStream = this._readyStream.asObservable(); + private readonly _chatStream: Subject = new Subject(); public readonly chatStream = this._chatStream.asObservable(); @@ -106,6 +110,9 @@ class IframeListener { private readonly _setTilesStream: Subject = new Subject(); public readonly setTilesStream = this._setTilesStream.asObservable(); + private readonly _setVariableStream: Subject = new Subject(); + public readonly setVariableStream = this._setVariableStream.asObservable(); + private readonly iframes = new Set(); private readonly iframeCloseCallbacks = new Map void)[]>(); private readonly scripts = new Map(); @@ -187,62 +194,76 @@ class IframeListener { }); } else if (isIframeEventWrapper(payload)) { - if (payload.type === "showLayer" && isLayerEvent(payload.data)) { - this._showLayerStream.next(payload.data); - } else if (payload.type === "hideLayer" && isLayerEvent(payload.data)) { - this._hideLayerStream.next(payload.data); - } else if (payload.type === "setProperty" && isSetPropertyEvent(payload.data)) { - this._setPropertyStream.next(payload.data); - } else if (payload.type === "chat" && isChatEvent(payload.data)) { - this._chatStream.next(payload.data); - } else if (payload.type === "openPopup" && isOpenPopupEvent(payload.data)) { - this._openPopupStream.next(payload.data); - } else if (payload.type === "closePopup" && isClosePopupEvent(payload.data)) { - this._closePopupStream.next(payload.data); - } else if (payload.type === "openTab" && isOpenTabEvent(payload.data)) { - scriptUtils.openTab(payload.data.url); - } else if (payload.type === "goToPage" && isGoToPageEvent(payload.data)) { - scriptUtils.goToPage(payload.data.url); - } else if (payload.type === "loadPage" && isLoadPageEvent(payload.data)) { - this._loadPageStream.next(payload.data.url); - } else if (payload.type === "playSound" && isPlaySoundEvent(payload.data)) { - this._playSoundStream.next(payload.data); - } else if (payload.type === "stopSound" && isStopSoundEvent(payload.data)) { - this._stopSoundStream.next(payload.data); - } else if (payload.type === "loadSound" && isLoadSoundEvent(payload.data)) { - this._loadSoundStream.next(payload.data); - } else if (payload.type === "openCoWebSite" && isOpenCoWebsite(payload.data)) { - scriptUtils.openCoWebsite( - payload.data.url, - foundSrc, - payload.data.allowApi, - payload.data.allowPolicy - ); - } else if (payload.type === "closeCoWebSite") { - scriptUtils.closeCoWebSite(); - } else if (payload.type === "disablePlayerControls") { - this._disablePlayerControlStream.next(); - } else if (payload.type === "restorePlayerControls") { - this._enablePlayerControlStream.next(); - } else if (payload.type === "displayBubble") { - this._displayBubbleStream.next(); - } else if (payload.type === "removeBubble") { - this._removeBubbleStream.next(); - } else if (payload.type == "onPlayerMove") { - this.sendPlayerMove = true; - } else if (payload.type == "getDataLayer") { - this._dataLayerChangeStream.next(); - } else if (isMenuItemRegisterIframeEvent(payload)) { - const data = payload.data.menutItem; - // @ts-ignore - this.iframeCloseCallbacks.get(iframe).push(() => { - this._unregisterMenuCommandStream.next(data); - }); - handleMenuItemRegistrationEvent(payload.data); - } else if (payload.type == "setTiles" && isSetTilesEvent(payload.data)) { - this._setTilesStream.next(payload.data); + if (payload.type === 'ready') { + this._readyStream.next(); + } else if (payload.type === "showLayer" && isLayerEvent(payload.data)) { + this._showLayerStream.next(payload.data); + } else if (payload.type === "hideLayer" && isLayerEvent(payload.data)) { + this._hideLayerStream.next(payload.data); + } else if (payload.type === "setProperty" && isSetPropertyEvent(payload.data)) { + this._setPropertyStream.next(payload.data); + } else if (payload.type === "chat" && isChatEvent(payload.data)) { + this._chatStream.next(payload.data); + } else if (payload.type === "openPopup" && isOpenPopupEvent(payload.data)) { + this._openPopupStream.next(payload.data); + } else if (payload.type === "closePopup" && isClosePopupEvent(payload.data)) { + this._closePopupStream.next(payload.data); + } else if (payload.type === "openTab" && isOpenTabEvent(payload.data)) { + scriptUtils.openTab(payload.data.url); + } else if (payload.type === "goToPage" && isGoToPageEvent(payload.data)) { + scriptUtils.goToPage(payload.data.url); + } else if (payload.type === "loadPage" && isLoadPageEvent(payload.data)) { + this._loadPageStream.next(payload.data.url); + } else if (payload.type === "playSound" && isPlaySoundEvent(payload.data)) { + this._playSoundStream.next(payload.data); + } else if (payload.type === "stopSound" && isStopSoundEvent(payload.data)) { + this._stopSoundStream.next(payload.data); + } else if (payload.type === "loadSound" && isLoadSoundEvent(payload.data)) { + this._loadSoundStream.next(payload.data); + } else if (payload.type === "openCoWebSite" && isOpenCoWebsite(payload.data)) { + scriptUtils.openCoWebsite( + payload.data.url, + foundSrc, + payload.data.allowApi, + payload.data.allowPolicy + ); + } else if (payload.type === "closeCoWebSite") { + scriptUtils.closeCoWebSite(); + } else if (payload.type === "disablePlayerControls") { + this._disablePlayerControlStream.next(); + } else if (payload.type === "restorePlayerControls") { + this._enablePlayerControlStream.next(); + } else if (payload.type === "displayBubble") { + this._displayBubbleStream.next(); + } else if (payload.type === "removeBubble") { + this._removeBubbleStream.next(); + } else if (payload.type == "onPlayerMove") { + this.sendPlayerMove = true; + } else if (payload.type == "getDataLayer") { + this._dataLayerChangeStream.next(); + } else if (isMenuItemRegisterIframeEvent(payload)) { + const data = payload.data.menutItem; + // @ts-ignore + this.iframeCloseCallbacks.get(iframe).push(() => { + this._unregisterMenuCommandStream.next(data); + }); + handleMenuItemRegistrationEvent(payload.data); + } else if (payload.type == "setTiles" && isSetTilesEvent(payload.data)) { + this._setTilesStream.next(payload.data); + } else if (isSetVariableIframeEvent(payload)) { + this._setVariableStream.next(payload.data); + + // Let's dispatch the message to the other iframes + for (iframe of this.iframes) { + if (iframe.contentWindow !== message.source) { + iframe.contentWindow?.postMessage({ + 'type': 'setVariable', + 'data': payload.data + }, '*'); + } } } + } }, false ); @@ -394,6 +415,13 @@ class IframeListener { }); } + setVariable(setVariableEvent: SetVariableEvent) { + this.postMessage({ + 'type': 'setVariable', + 'data': setVariableEvent + }); + } + /** * Sends the message... to all allowed iframes. */ diff --git a/front/src/Api/iframe/room.ts b/front/src/Api/iframe/room.ts index c70d0aad..623773c3 100644 --- a/front/src/Api/iframe/room.ts +++ b/front/src/Api/iframe/room.ts @@ -1,4 +1,4 @@ -import { Subject } from "rxjs"; +import {Observable, Subject} from "rxjs"; import { isDataLayerEvent } from "../Events/DataLayerEvent"; import { EnterLeaveEvent, isEnterLeaveEvent } from "../Events/EnterLeaveEvent"; @@ -6,6 +6,9 @@ import { isGameStateEvent } from "../Events/GameStateEvent"; import {IframeApiContribution, queryWorkadventure, sendToWorkadventure} from "./IframeApiContribution"; import { apiCallback } from "./registeredCallbacks"; +import type {LayerEvent} from "../Events/LayerEvent"; +import type {SetPropertyEvent} from "../Events/setPropertyEvent"; +import {isSetVariableEvent, SetVariableEvent} from "../Events/SetVariableEvent"; import type { ITiledMap } from "../../Phaser/Map/ITiledMap"; import type { DataLayerEvent } from "../Events/DataLayerEvent"; @@ -15,6 +18,9 @@ const enterStreams: Map> = new Map> = new Map>(); const dataLayerResolver = new Subject(); const stateResolvers = new Subject(); +const setVariableResolvers = new Subject(); +const variables = new Map(); +const variableSubscribers = new Map>(); let immutableDataPromise: Promise | undefined = undefined; @@ -52,6 +58,14 @@ function getDataLayer(): Promise { }); } +setVariableResolvers.subscribe((event) => { + variables.set(event.key, event.value); + const subject = variableSubscribers.get(event.key); + if (subject !== undefined) { + subject.next(event.value); + } +}); + export class WorkadventureRoomCommands extends IframeApiContribution { callbacks = [ apiCallback({ @@ -75,6 +89,13 @@ export class WorkadventureRoomCommands extends IframeApiContribution { + setVariableResolvers.next(payloadData); + } + }), ]; onEnterZone(name: string, callback: () => void): void { @@ -132,6 +153,30 @@ export class WorkadventureRoomCommands extends IframeApiContribution { + let subject = variableSubscribers.get(key); + if (subject === undefined) { + subject = new Subject(); + variableSubscribers.set(key, subject); + } + return subject.asObservable(); + } } export default new WorkadventureRoomCommands(); diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index d767f0f4..c2a2b38d 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -91,6 +91,8 @@ import { soundManager } from "./SoundManager"; import { peerStore, screenSharingPeerStore } from "../../Stores/PeerStore"; import { videoFocusStore } from "../../Stores/VideoFocusStore"; import { biggestAvailableAreaStore } from "../../Stores/BiggestAvailableAreaStore"; +import { SharedVariablesManager } from "./SharedVariablesManager"; +import type {InitEvent} from "../../Api/Events/InitEvent"; export interface GameSceneInitInterface { initPosition: PointInterface | null; @@ -199,7 +201,8 @@ export class GameScene extends DirtyScene { private mapTransitioning: boolean = false; //used to prevent transitions happenning at the same time. private emoteManager!: EmoteManager; private preloading: boolean = true; - startPositionCalculator!: StartPositionCalculator; + private startPositionCalculator!: StartPositionCalculator; + private sharedVariablesManager!: SharedVariablesManager; constructor(private room: Room, MapUrlFile: string, customKey?: string | undefined) { super({ @@ -396,6 +399,23 @@ export class GameScene extends DirtyScene { }); } + + this.iframeSubscriptionList.push(iframeListener.readyStream.subscribe((iframe) => { + this.connectionAnswerPromise.then(connection => { + // Generate init message for an iframe + // TODO: merge with GameStateEvent + const initEvent: InitEvent = { + variables: this.sharedVariablesManager.variables + } + + }); + // TODO: SEND INIT MESSAGE TO IFRAMES ONLY WHEN CONNECTION IS ESTABLISHED + // TODO: SEND INIT MESSAGE TO IFRAMES ONLY WHEN CONNECTION IS ESTABLISHED + // TODO: SEND INIT MESSAGE TO IFRAMES ONLY WHEN CONNECTION IS ESTABLISHED + // TODO: SEND INIT MESSAGE TO IFRAMES ONLY WHEN CONNECTION IS ESTABLISHED + // TODO: SEND INIT MESSAGE TO IFRAMES ONLY WHEN CONNECTION IS ESTABLISHED + })); + // Now, let's load the script, if any const scripts = this.getScriptUrls(this.mapFile); for (const script of scripts) { @@ -706,6 +726,9 @@ export class GameScene extends DirtyScene { this.gameMap.setPosition(event.x, event.y); }); + // Set up variables manager + this.sharedVariablesManager = new SharedVariablesManager(this.connection, this.gameMap); + //this.initUsersPosition(roomJoinedMessage.users); this.connectionAnswerPromiseResolve(onConnect.room); // Analyze tags to find if we are admin. If yes, show console. @@ -1148,6 +1171,7 @@ ${escapedMessage} this.peerStoreUnsubscribe(); this.biggestAvailableAreaStoreUnsubscribe(); iframeListener.unregisterAnswerer('getState'); + this.sharedVariablesManager?.close(); mediaManager.hideGameOverlay(); diff --git a/front/src/Phaser/Game/SharedVariablesManager.ts b/front/src/Phaser/Game/SharedVariablesManager.ts new file mode 100644 index 00000000..abd2474e --- /dev/null +++ b/front/src/Phaser/Game/SharedVariablesManager.ts @@ -0,0 +1,59 @@ +/** + * Handles variables shared between the scripting API and the server. + */ +import type {RoomConnection} from "../../Connexion/RoomConnection"; +import {iframeListener} from "../../Api/IframeListener"; +import type {Subscription} from "rxjs"; +import type {GameMap} from "./GameMap"; +import type {ITiledMapObject} from "../Map/ITiledMap"; + +export class SharedVariablesManager { + private _variables = new Map(); + private iframeListenerSubscription: Subscription; + private variableObjects: Map; + + constructor(private roomConnection: RoomConnection, private gameMap: GameMap) { + // We initialize the list of variable object at room start. The objects cannot be edited later + // (otherwise, this would cause a security issue if the scripting API can edit this list of objects) + this.variableObjects = SharedVariablesManager.findVariablesInMap(gameMap); + + // When a variable is modified from an iFrame + this.iframeListenerSubscription = iframeListener.setVariableStream.subscribe((event) => { + const key = event.key; + + if (!this.variableObjects.has(key)) { + const errMsg = 'A script is trying to modify variable "'+key+'" but this variable is not defined in the map.' + + 'There should be an object in the map whose name is "'+key+'" and whose type is "variable"'; + console.error(errMsg); + throw new Error(errMsg); + } + + this._variables.set(key, event.value); + // TODO: dispatch to the room connection. + }); + } + + private static findVariablesInMap(gameMap: GameMap): Map { + const objects = new Map(); + for (const layer of gameMap.getMap().layers) { + if (layer.type === 'objectgroup') { + for (const object of layer.objects) { + if (object.type === 'variable') { + // We store a copy of the object (to make it immutable) + objects.set(object.name, {...object}); + } + } + } + } + return objects; + } + + + public close(): void { + this.iframeListenerSubscription.unsubscribe(); + } + + get variables(): Map { + return this._variables; + } +} diff --git a/front/src/iframe_api.ts b/front/src/iframe_api.ts index 1915020e..b27bda2d 100644 --- a/front/src/iframe_api.ts +++ b/front/src/iframe_api.ts @@ -206,3 +206,9 @@ window.addEventListener( // ... } ); + +// Notify WorkAdventure that we are ready to receive data +sendToWorkadventure({ + type: 'ready', + data: null +}); diff --git a/maps/tests/Variables/script.js b/maps/tests/Variables/script.js new file mode 100644 index 00000000..afd16773 --- /dev/null +++ b/maps/tests/Variables/script.js @@ -0,0 +1,11 @@ + +console.log('Trying to set variable "not_exists". This should display an error in the console.') +WA.room.saveVariable('not_exists', 'foo'); + +console.log('Trying to set variable "config". This should work.'); +WA.room.saveVariable('config', {'foo': 'bar'}); + +console.log('Trying to read variable "config". This should display a {"foo": "bar"} object.'); +console.log(WA.room.loadVariable('config')); + + diff --git a/maps/tests/Variables/variables.json b/maps/tests/Variables/variables.json new file mode 100644 index 00000000..93573da8 --- /dev/null +++ b/maps/tests/Variables/variables.json @@ -0,0 +1,112 @@ +{ "compressionlevel":-1, + "height":10, + "infinite":false, + "layers":[ + { + "data":[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + "height":10, + "id":1, + "name":"floor", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "data":[0, 0, 0, 0, 0, 0, 0, 0, 23, 23, 0, 0, 0, 0, 0, 0, 0, 0, 23, 23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "height":10, + "id":6, + "name":"triggerZone", + "opacity":1, + "properties":[ + { + "name":"zone", + "type":"string", + "value":"myTrigger" + }], + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "height":10, + "id":2, + "name":"start", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":3, + "name":"floorLayer", + "objects":[ + { + "height":67, + "id":3, + "name":"", + "rotation":0, + "text": + { + "fontfamily":"Sans Serif", + "pixelsize":11, + "text":"Test:\nTODO", + "wrap":true + }, + "type":"", + "visible":true, + "width":252.4375, + "x":2.78125, + "y":2.5 + }, + { + "id":5, + "template":"config.tx", + "x":57.5, + "y":111 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":8, + "nextobjectid":6, + "orientation":"orthogonal", + "properties":[ + { + "name":"script", + "type":"string", + "value":"script.js" + }], + "renderorder":"right-down", + "tiledversion":"2021.03.23", + "tileheight":32, + "tilesets":[ + { + "columns":11, + "firstgid":1, + "image":"..\/tileset1.png", + "imageheight":352, + "imagewidth":352, + "margin":0, + "name":"tileset1", + "spacing":0, + "tilecount":121, + "tileheight":32, + "tilewidth":32 + }], + "tilewidth":32, + "type":"map", + "version":1.5, + "width":10 +} \ No newline at end of file diff --git a/maps/tests/index.html b/maps/tests/index.html index 38ee51ef..a96690b8 100644 --- a/maps/tests/index.html +++ b/maps/tests/index.html @@ -202,6 +202,14 @@ Test set tiles + + + Success Failure Pending + + + Testing scripting variables + + - - - - - \ No newline at end of file diff --git a/maps/tests/Metadata/getCurrentRoom.js b/maps/tests/Metadata/getCurrentRoom.js new file mode 100644 index 00000000..8e90a4ae --- /dev/null +++ b/maps/tests/Metadata/getCurrentRoom.js @@ -0,0 +1,11 @@ +WA.onInit().then(() => { + console.log('id: ', WA.room.id); + console.log('Map URL: ', WA.room.mapURL); + console.log('Player name: ', WA.player.name); + console.log('Player id: ', WA.player.id); + console.log('Player tags: ', WA.player.tags); +}); + +WA.room.getMap().then((data) => { + console.log('Map data', data); +}) diff --git a/maps/tests/Metadata/getCurrentRoom.json b/maps/tests/Metadata/getCurrentRoom.json index c14bb946..05591521 100644 --- a/maps/tests/Metadata/getCurrentRoom.json +++ b/maps/tests/Metadata/getCurrentRoom.json @@ -1,11 +1,4 @@ { "compressionlevel":-1, - "editorsettings": - { - "export": - { - "target":"." - } - }, "height":10, "infinite":false, "layers":[ @@ -51,29 +44,6 @@ "x":0, "y":0 }, - { - "data":[0, 0, 0, 0, 0, 0, 128, 128, 128, 128, 0, 0, 0, 0, 0, 0, 128, 128, 128, 128, 0, 0, 0, 0, 0, 0, 128, 128, 128, 128, 0, 0, 0, 0, 0, 0, 128, 128, 128, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - "height":10, - "id":4, - "name":"metadata", - "opacity":1, - "properties":[ - { - "name":"openWebsite", - "type":"string", - "value":"getCurrentRoom.html" - }, - { - "name":"openWebsiteAllowApi", - "type":"bool", - "value":true - }], - "type":"tilelayer", - "visible":true, - "width":10, - "x":0, - "y":0 - }, { "draworder":"topdown", "id":5, @@ -88,7 +58,7 @@ { "fontfamily":"Sans Serif", "pixelsize":9, - "text":"Test : \nWalk on the grass and open the console.\n\nResult : \nYou should see a console.log() of the following attributes : \n\t- id : ID of the current room\n\t- map : data of the JSON file of the map\n\t- mapUrl : url of the JSON file of the map\n\t- startLayer : Name of the layer where the current user started (HereYouAppered)\n\n\n", + "text":"Test : \nOpen the console.\n\nResult : \nYou should see a console.log() of the following attributes : \n\t- id : ID of the current room\n\t- mapUrl : url of the JSON file of the map\n\t- Player name\n - Player ID\n - Player tags\n\nAnd also:\n\t- map : data of the JSON file of the map\n\n", "wrap":true }, "type":"", @@ -106,8 +76,14 @@ "nextlayerid":11, "nextobjectid":2, "orientation":"orthogonal", + "properties":[ + { + "name":"script", + "type":"string", + "value":"getCurrentRoom.js" + }], "renderorder":"right-down", - "tiledversion":"1.4.3", + "tiledversion":"2021.03.23", "tileheight":32, "tilesets":[ { @@ -274,6 +250,6 @@ }], "tilewidth":32, "type":"map", - "version":1.4, + "version":1.5, "width":10 } \ No newline at end of file diff --git a/maps/tests/Metadata/getCurrentUser.html b/maps/tests/Metadata/getCurrentUser.html deleted file mode 100644 index 02be24f7..00000000 --- a/maps/tests/Metadata/getCurrentUser.html +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/maps/tests/Metadata/getCurrentUser.json b/maps/tests/Metadata/getCurrentUser.json deleted file mode 100644 index 9efd0d09..00000000 --- a/maps/tests/Metadata/getCurrentUser.json +++ /dev/null @@ -1,296 +0,0 @@ -{ "compressionlevel":-1, - "editorsettings": - { - "export": - { - "target":"." - } - }, - "height":10, - "infinite":false, - "layers":[ - { - "data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 52, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - "height":10, - "id":1, - "name":"start", - "opacity":1, - "type":"tilelayer", - "visible":true, - "width":10, - "x":0, - "y":0 - }, - { - "data":[33, 34, 34, 34, 34, 34, 34, 34, 34, 35, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 49, 50, 50, 50, 50, 50, 50, 50, 50, 51], - "height":10, - "id":2, - "name":"bottom", - "opacity":1, - "type":"tilelayer", - "visible":true, - "width":10, - "x":0, - "y":0 - }, - { - "data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 101, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - "height":10, - "id":9, - "name":"exit", - "opacity":1, - "properties":[ - { - "name":"exitUrl", - "type":"string", - "value":"getCurrentRoom.json#HereYouAppered" - }], - "type":"tilelayer", - "visible":true, - "width":10, - "x":0, - "y":0 - }, - { - "data":[0, 0, 0, 0, 0, 0, 128, 128, 128, 128, 0, 0, 0, 0, 0, 0, 128, 128, 128, 128, 0, 0, 0, 0, 0, 0, 128, 128, 128, 128, 0, 0, 0, 0, 0, 0, 128, 128, 128, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - "height":10, - "id":4, - "name":"metadata", - "opacity":1, - "properties":[ - { - "name":"openWebsite", - "type":"string", - "value":"getCurrentUser.html" - }, - { - "name":"openWebsiteAllowApi", - "type":"bool", - "value":true - }], - "type":"tilelayer", - "visible":true, - "width":10, - "x":0, - "y":0 - }, - { - "draworder":"topdown", - "id":5, - "name":"floorLayer", - "objects":[ - { - "height":151.839293303871, - "id":1, - "name":"", - "rotation":0, - "text": - { - "fontfamily":"Sans Serif", - "pixelsize":9, - "text":"Test : \nWalk on the grass, open the console.\n\nResut : \nYou should see a console.log() of the following attributes :\n\t- id : ID of the current user\n\t- nickName : Name of the current user\n\t- tags : List of tags of the current user\n\nFinally : \nWalk on the red tile and continue the test in an another room.", - "wrap":true - }, - "type":"", - "visible":true, - "width":305.097705765524, - "x":14.750638909983, - "y":159.621625296353 - }], - "opacity":1, - "type":"objectgroup", - "visible":true, - "x":0, - "y":0 - }], - "nextlayerid":10, - "nextobjectid":2, - "orientation":"orthogonal", - "renderorder":"right-down", - "tiledversion":"1.4.3", - "tileheight":32, - "tilesets":[ - { - "columns":8, - "firstgid":1, - "image":"tileset_dungeon.png", - "imageheight":256, - "imagewidth":256, - "margin":0, - "name":"TDungeon", - "spacing":0, - "tilecount":64, - "tileheight":32, - "tiles":[ - { - "id":0, - "properties":[ - { - "name":"collides", - "type":"bool", - "value":true - }] - }, - { - "id":1, - "properties":[ - { - "name":"collides", - "type":"bool", - "value":true - }] - }, - { - "id":2, - "properties":[ - { - "name":"collides", - "type":"bool", - "value":true - }] - }, - { - "id":3, - "properties":[ - { - "name":"collides", - "type":"bool", - "value":true - }] - }, - { - "id":4, - "properties":[ - { - "name":"collides", - "type":"bool", - "value":true - }] - }, - { - "id":8, - "properties":[ - { - "name":"collides", - "type":"bool", - "value":true - }] - }, - { - "id":9, - "properties":[ - { - "name":"collides", - "type":"bool", - "value":true - }] - }, - { - "id":10, - "properties":[ - { - "name":"collides", - "type":"bool", - "value":true - }] - }, - { - "id":11, - "properties":[ - { - "name":"collides", - "type":"bool", - "value":true - }] - }, - { - "id":12, - "properties":[ - { - "name":"collides", - "type":"bool", - "value":true - }] - }, - { - "id":16, - "properties":[ - { - "name":"collides", - "type":"bool", - "value":true - }] - }, - { - "id":17, - "properties":[ - { - "name":"collides", - "type":"bool", - "value":true - }] - }, - { - "id":18, - "properties":[ - { - "name":"collides", - "type":"bool", - "value":true - }] - }, - { - "id":19, - "properties":[ - { - "name":"collides", - "type":"bool", - "value":true - }] - }, - { - "id":20, - "properties":[ - { - "name":"collides", - "type":"bool", - "value":true - }] - }], - "tilewidth":32 - }, - { - "columns":8, - "firstgid":65, - "image":"floortileset.png", - "imageheight":288, - "imagewidth":256, - "margin":0, - "name":"Floor", - "spacing":0, - "tilecount":72, - "tileheight":32, - "tiles":[ - { - "animation":[ - { - "duration":100, - "tileid":9 - }, - { - "duration":100, - "tileid":64 - }, - { - "duration":100, - "tileid":55 - }], - "id":0 - }], - "tilewidth":32 - }], - "tilewidth":32, - "type":"map", - "version":1.4, - "width":10 -} \ No newline at end of file diff --git a/maps/tests/index.html b/maps/tests/index.html index a96690b8..dbcf8287 100644 --- a/maps/tests/index.html +++ b/maps/tests/index.html @@ -127,15 +127,7 @@ Success Failure Pending - Testing return current room attributes by Scripting API (Need to test from current user) - - - - - Success Failure Pending - - - Testing return current user attributes by Scripting API + Testing room/player attributes in Scripting API + WA.onInit From c30de8c6db35a726bb1dd7f49b84fa7631843f14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Mon, 5 Jul 2021 17:25:23 +0200 Subject: [PATCH 006/101] Adding support for default variables values --- front/src/Api/Events/IframeEvent.ts | 4 +-- front/src/Api/iframe/room.ts | 16 +++++---- front/src/Phaser/Game/GameMap.ts | 8 ++--- front/src/Phaser/Game/GameScene.ts | 10 +++--- .../src/Phaser/Game/SharedVariablesManager.ts | 35 ++++++++++++++++--- .../Phaser/Game/StartPositionCalculator.ts | 6 ++-- front/src/Phaser/Map/ITiledMap.ts | 16 ++++----- front/src/iframe_api.ts | 3 +- maps/tests/Variables/script.js | 18 +++++----- maps/tests/Variables/variables.json | 20 ++++++++++- 10 files changed, 92 insertions(+), 44 deletions(-) diff --git a/front/src/Api/Events/IframeEvent.ts b/front/src/Api/Events/IframeEvent.ts index 613ae525..a0e7717a 100644 --- a/front/src/Api/Events/IframeEvent.ts +++ b/front/src/Api/Events/IframeEvent.ts @@ -88,13 +88,11 @@ export type IframeQueryMap = { getState: { query: undefined, answer: GameStateEvent, - callback: () => GameStateEvent|PromiseLike }, getMapData: { query: undefined, answer: MapDataEvent, - callback: () => MapDataEvent|PromiseLike - } + }, } export interface IframeQuery { diff --git a/front/src/Api/iframe/room.ts b/front/src/Api/iframe/room.ts index 2e4f9fd5..00d974dc 100644 --- a/front/src/Api/iframe/room.ts +++ b/front/src/Api/iframe/room.ts @@ -1,18 +1,12 @@ import {Observable, Subject} from "rxjs"; -import { isMapDataEvent } from "../Events/MapDataEvent"; import { EnterLeaveEvent, isEnterLeaveEvent } from "../Events/EnterLeaveEvent"; -import { isGameStateEvent } from "../Events/GameStateEvent"; import {IframeApiContribution, queryWorkadventure, sendToWorkadventure} from "./IframeApiContribution"; import { apiCallback } from "./registeredCallbacks"; -import type {LayerEvent} from "../Events/LayerEvent"; -import type {SetPropertyEvent} from "../Events/setPropertyEvent"; import {isSetVariableEvent, SetVariableEvent} from "../Events/SetVariableEvent"; import type { ITiledMap } from "../../Phaser/Map/ITiledMap"; -import type { MapDataEvent } from "../Events/MapDataEvent"; -import type { GameStateEvent } from "../Events/GameStateEvent"; const enterStreams: Map> = new Map>(); const leaveStreams: Map> = new Map>(); @@ -39,6 +33,16 @@ export const setMapURL = (url: string) => { mapURL = url; } +export const initVariables = (_variables: Map): void => { + for (const [name, value] of _variables.entries()) { + // In case the user already decided to put values in the variables (before onInit), let's make sure onInit does not override this. + if (!variables.has(name)) { + variables.set(name, value); + } + } + +} + setVariableResolvers.subscribe((event) => { variables.set(event.key, event.value); const subject = variableSubscribers.get(event.key); diff --git a/front/src/Phaser/Game/GameMap.ts b/front/src/Phaser/Game/GameMap.ts index a616cf4a..e095dab1 100644 --- a/front/src/Phaser/Game/GameMap.ts +++ b/front/src/Phaser/Game/GameMap.ts @@ -1,4 +1,4 @@ -import type { ITiledMap, ITiledMapLayer, ITiledMapLayerProperty } from "../Map/ITiledMap"; +import type { ITiledMap, ITiledMapLayer, ITiledMapProperty } from "../Map/ITiledMap"; import { flattenGroupLayersMap } from "../Map/LayersFlattener"; import TilemapLayer = Phaser.Tilemaps.TilemapLayer; import { DEPTH_OVERLAY_INDEX } from "./DepthIndexes"; @@ -19,7 +19,7 @@ export class GameMap { private callbacks = new Map>(); private tileNameMap = new Map(); - private tileSetPropertyMap: { [tile_index: number]: Array } = {}; + private tileSetPropertyMap: { [tile_index: number]: Array } = {}; public readonly flatLayers: ITiledMapLayer[]; public readonly phaserLayers: TilemapLayer[] = []; @@ -61,7 +61,7 @@ export class GameMap { } } - public getPropertiesForIndex(index: number): Array { + public getPropertiesForIndex(index: number): Array { if (this.tileSetPropertyMap[index]) { return this.tileSetPropertyMap[index]; } @@ -151,7 +151,7 @@ export class GameMap { return this.map; } - private getTileProperty(index: number): Array { + private getTileProperty(index: number): Array { return this.tileSetPropertyMap[index]; } diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 1c522703..5d4c6b2b 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -50,7 +50,7 @@ import { SelectCharacterScene, SelectCharacterSceneName } from "../Login/SelectC import type { ITiledMap, ITiledMapLayer, - ITiledMapLayerProperty, + ITiledMapProperty, ITiledMapObject, ITiledTileSet, } from "../Map/ITiledMap"; @@ -1197,12 +1197,12 @@ ${escapedMessage} } private getProperty(layer: ITiledMapLayer | ITiledMap, name: string): string | boolean | number | undefined { - const properties: ITiledMapLayerProperty[] | undefined = layer.properties; + const properties: ITiledMapProperty[] | undefined = layer.properties; if (!properties) { return undefined; } const obj = properties.find( - (property: ITiledMapLayerProperty) => property.name.toLowerCase() === name.toLowerCase() + (property: ITiledMapProperty) => property.name.toLowerCase() === name.toLowerCase() ); if (obj === undefined) { return undefined; @@ -1211,12 +1211,12 @@ ${escapedMessage} } private getProperties(layer: ITiledMapLayer | ITiledMap, name: string): (string | number | boolean | undefined)[] { - const properties: ITiledMapLayerProperty[] | undefined = layer.properties; + const properties: ITiledMapProperty[] | undefined = layer.properties; if (!properties) { return []; } return properties - .filter((property: ITiledMapLayerProperty) => property.name.toLowerCase() === name.toLowerCase()) + .filter((property: ITiledMapProperty) => property.name.toLowerCase() === name.toLowerCase()) .map((property) => property.value); } diff --git a/front/src/Phaser/Game/SharedVariablesManager.ts b/front/src/Phaser/Game/SharedVariablesManager.ts index abd2474e..aeb26d68 100644 --- a/front/src/Phaser/Game/SharedVariablesManager.ts +++ b/front/src/Phaser/Game/SharedVariablesManager.ts @@ -5,18 +5,28 @@ import type {RoomConnection} from "../../Connexion/RoomConnection"; import {iframeListener} from "../../Api/IframeListener"; import type {Subscription} from "rxjs"; import type {GameMap} from "./GameMap"; -import type {ITiledMapObject} from "../Map/ITiledMap"; +import type {ITile, ITiledMapObject} from "../Map/ITiledMap"; +import type {Var} from "svelte/types/compiler/interfaces"; + +interface Variable { + defaultValue: unknown +} export class SharedVariablesManager { private _variables = new Map(); private iframeListenerSubscription: Subscription; - private variableObjects: Map; + private variableObjects: Map; constructor(private roomConnection: RoomConnection, private gameMap: GameMap) { // We initialize the list of variable object at room start. The objects cannot be edited later // (otherwise, this would cause a security issue if the scripting API can edit this list of objects) this.variableObjects = SharedVariablesManager.findVariablesInMap(gameMap); + // Let's initialize default values + for (const [name, variableObject] of this.variableObjects.entries()) { + this._variables.set(name, variableObject.defaultValue); + } + // When a variable is modified from an iFrame this.iframeListenerSubscription = iframeListener.setVariableStream.subscribe((event) => { const key = event.key; @@ -33,14 +43,14 @@ export class SharedVariablesManager { }); } - private static findVariablesInMap(gameMap: GameMap): Map { - const objects = new Map(); + private static findVariablesInMap(gameMap: GameMap): Map { + const objects = new Map(); for (const layer of gameMap.getMap().layers) { if (layer.type === 'objectgroup') { for (const object of layer.objects) { if (object.type === 'variable') { // We store a copy of the object (to make it immutable) - objects.set(object.name, {...object}); + objects.set(object.name, this.iTiledObjectToVariable(object)); } } } @@ -48,6 +58,21 @@ export class SharedVariablesManager { return objects; } + private static iTiledObjectToVariable(object: ITiledMapObject): Variable { + const variable: Variable = { + defaultValue: undefined + }; + + if (object.properties) { + for (const property of object.properties) { + if (property.name === 'default') { + variable.defaultValue = property.value; + } + } + } + + return variable; + } public close(): void { this.iframeListenerSubscription.unsubscribe(); diff --git a/front/src/Phaser/Game/StartPositionCalculator.ts b/front/src/Phaser/Game/StartPositionCalculator.ts index 7460c81c..a0184d2b 100644 --- a/front/src/Phaser/Game/StartPositionCalculator.ts +++ b/front/src/Phaser/Game/StartPositionCalculator.ts @@ -1,5 +1,5 @@ import type { PositionInterface } from "../../Connexion/ConnexionModels"; -import type { ITiledMap, ITiledMapLayer, ITiledMapLayerProperty, ITiledMapTileLayer } from "../Map/ITiledMap"; +import type { ITiledMap, ITiledMapLayer, ITiledMapProperty, ITiledMapTileLayer } from "../Map/ITiledMap"; import type { GameMap } from "./GameMap"; const defaultStartLayerName = "start"; @@ -112,12 +112,12 @@ export class StartPositionCalculator { } private getProperty(layer: ITiledMapLayer | ITiledMap, name: string): string | boolean | number | undefined { - const properties: ITiledMapLayerProperty[] | undefined = layer.properties; + const properties: ITiledMapProperty[] | undefined = layer.properties; if (!properties) { return undefined; } const obj = properties.find( - (property: ITiledMapLayerProperty) => property.name.toLowerCase() === name.toLowerCase() + (property: ITiledMapProperty) => property.name.toLowerCase() === name.toLowerCase() ); if (obj === undefined) { return undefined; diff --git a/front/src/Phaser/Map/ITiledMap.ts b/front/src/Phaser/Map/ITiledMap.ts index 0653e83a..c5b96f22 100644 --- a/front/src/Phaser/Map/ITiledMap.ts +++ b/front/src/Phaser/Map/ITiledMap.ts @@ -16,7 +16,7 @@ export interface ITiledMap { * Map orientation (orthogonal) */ orientation: string; - properties?: ITiledMapLayerProperty[]; + properties?: ITiledMapProperty[]; /** * Render order (right-down) @@ -33,7 +33,7 @@ export interface ITiledMap { type?: string; } -export interface ITiledMapLayerProperty { +export interface ITiledMapProperty { name: string; type: string; value: string | boolean | number | undefined; @@ -51,7 +51,7 @@ export interface ITiledMapGroupLayer { id?: number; name: string; opacity: number; - properties?: ITiledMapLayerProperty[]; + properties?: ITiledMapProperty[]; type: "group"; visible: boolean; @@ -69,7 +69,7 @@ export interface ITiledMapTileLayer { height: number; name: string; opacity: number; - properties?: ITiledMapLayerProperty[]; + properties?: ITiledMapProperty[]; encoding?: string; compression?: string; @@ -91,7 +91,7 @@ export interface ITiledMapObjectLayer { height: number; name: string; opacity: number; - properties?: ITiledMapLayerProperty[]; + properties?: ITiledMapProperty[]; encoding?: string; compression?: string; @@ -117,7 +117,7 @@ export interface ITiledMapObject { gid: number; height: number; name: string; - properties: { [key: string]: string }; + properties?: ITiledMapProperty[]; rotation: number; type: string; visible: boolean; @@ -163,7 +163,7 @@ export interface ITiledTileSet { imagewidth: number; margin: number; name: string; - properties: { [key: string]: string }; + properties?: ITiledMapProperty[]; spacing: number; tilecount: number; tileheight: number; @@ -182,7 +182,7 @@ export interface ITile { id: number; type?: string; - properties?: Array; + properties?: ITiledMapProperty[]; } export interface ITiledMapTerrain { diff --git a/front/src/iframe_api.ts b/front/src/iframe_api.ts index ee68270e..fb44738f 100644 --- a/front/src/iframe_api.ts +++ b/front/src/iframe_api.ts @@ -11,7 +11,7 @@ import nav from "./Api/iframe/nav"; import controls from "./Api/iframe/controls"; import ui from "./Api/iframe/ui"; import sound from "./Api/iframe/sound"; -import room, {setMapURL, setRoomId} from "./Api/iframe/room"; +import room, {initVariables, setMapURL, setRoomId} from "./Api/iframe/room"; import player, {setPlayerName, setTags, setUuid} from "./Api/iframe/player"; import type { ButtonDescriptor } from "./Api/iframe/Ui/ButtonDescriptor"; import type { Popup } from "./Api/iframe/Ui/Popup"; @@ -29,6 +29,7 @@ const initPromise = new Promise((resolve) => { setMapURL(state.mapUrl); setTags(state.tags); setUuid(state.uuid); + initVariables(state.variables as Map); resolve(); })); }); diff --git a/maps/tests/Variables/script.js b/maps/tests/Variables/script.js index afd16773..cef9818e 100644 --- a/maps/tests/Variables/script.js +++ b/maps/tests/Variables/script.js @@ -1,11 +1,13 @@ +WA.onInit().then(() => { + console.log('Trying to read variable "doorOpened" whose default property is true. This should display "true".'); + console.log('doorOpened', WA.room.loadVariable('doorOpened')); -console.log('Trying to set variable "not_exists". This should display an error in the console.') -WA.room.saveVariable('not_exists', 'foo'); - -console.log('Trying to set variable "config". This should work.'); -WA.room.saveVariable('config', {'foo': 'bar'}); - -console.log('Trying to read variable "config". This should display a {"foo": "bar"} object.'); -console.log(WA.room.loadVariable('config')); + console.log('Trying to set variable "not_exists". This should display an error in the console.') + WA.room.saveVariable('not_exists', 'foo'); + console.log('Trying to set variable "config". This should work.'); + WA.room.saveVariable('config', {'foo': 'bar'}); + console.log('Trying to read variable "config". This should display a {"foo": "bar"} object.'); + console.log(WA.room.loadVariable('config')); +}); diff --git a/maps/tests/Variables/variables.json b/maps/tests/Variables/variables.json index 93573da8..61067071 100644 --- a/maps/tests/Variables/variables.json +++ b/maps/tests/Variables/variables.json @@ -72,6 +72,24 @@ "template":"config.tx", "x":57.5, "y":111 + }, + { + "height":0, + "id":6, + "name":"doorOpened", + "point":true, + "properties":[ + { + "name":"default", + "type":"bool", + "value":true + }], + "rotation":0, + "type":"variable", + "visible":true, + "width":0, + "x":131.38069962269, + "y":106.004988169086 }], "opacity":1, "type":"objectgroup", @@ -80,7 +98,7 @@ "y":0 }], "nextlayerid":8, - "nextobjectid":6, + "nextobjectid":8, "orientation":"orthogonal", "properties":[ { From bf17ad4567b2c07638e7ade47f3b33343cda92f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Mon, 5 Jul 2021 18:29:34 +0200 Subject: [PATCH 007/101] Switching setVariable to a query and fixing error hangling in query mechanism --- front/src/Api/Events/IframeEvent.ts | 5 +- front/src/Api/IframeListener.ts | 48 ++++++++++--------- front/src/Api/iframe/room.ts | 4 +- .../src/Phaser/Game/SharedVariablesManager.ts | 5 +- front/src/iframe_api.ts | 24 +++++----- maps/tests/Variables/script.js | 6 ++- 6 files changed, 49 insertions(+), 43 deletions(-) diff --git a/front/src/Api/Events/IframeEvent.ts b/front/src/Api/Events/IframeEvent.ts index a0e7717a..54319fd3 100644 --- a/front/src/Api/Events/IframeEvent.ts +++ b/front/src/Api/Events/IframeEvent.ts @@ -50,7 +50,6 @@ export type IframeEventMap = { getState: undefined; registerMenuCommand: MenuItemRegisterEvent; setTiles: SetTilesEvent; - setVariable: SetVariableEvent; }; export interface IframeEvent { type: T; @@ -93,6 +92,10 @@ export type IframeQueryMap = { query: undefined, answer: MapDataEvent, }, + setVariable: { + query: SetVariableEvent, + answer: void + } } export interface IframeQuery { diff --git a/front/src/Api/IframeListener.ts b/front/src/Api/IframeListener.ts index c74e68a7..ee969721 100644 --- a/front/src/Api/IframeListener.ts +++ b/front/src/Api/IframeListener.ts @@ -105,9 +105,6 @@ class IframeListener { private readonly _setTilesStream: Subject = new Subject(); public readonly setTilesStream = this._setTilesStream.asObservable(); - private readonly _setVariableStream: Subject = new Subject(); - public readonly setVariableStream = this._setVariableStream.asObservable(); - private readonly iframes = new Set(); private readonly iframeCloseCallbacks = new Map void)[]>(); private readonly scripts = new Map(); @@ -171,13 +168,7 @@ class IframeListener { return; } - Promise.resolve(answerer(query.data)).then((value) => { - iframe?.contentWindow?.postMessage({ - id: queryId, - type: query.type, - data: value - }, '*'); - }).catch(reason => { + const errorHandler = (reason: any) => { console.error('An error occurred while responding to an iFrame query.', reason); let reasonMsg: string; if (reason instanceof Error) { @@ -191,8 +182,31 @@ class IframeListener { type: query.type, error: reasonMsg } as IframeErrorAnswerEvent, '*'); - }); + }; + try { + Promise.resolve(answerer(query.data)).then((value) => { + iframe?.contentWindow?.postMessage({ + id: queryId, + type: query.type, + data: value + }, '*'); + }).catch(errorHandler); + } catch (reason) { + errorHandler(reason); + } + + if (isSetVariableIframeEvent(payload.query)) { + // Let's dispatch the message to the other iframes + for (iframe of this.iframes) { + if (iframe.contentWindow !== message.source) { + iframe.contentWindow?.postMessage({ + 'type': 'setVariable', + 'data': payload.query.data + }, '*'); + } + } + } } else if (isIframeEventWrapper(payload)) { if (payload.type === "showLayer" && isLayerEvent(payload.data)) { this._showLayerStream.next(payload.data); @@ -246,18 +260,6 @@ class IframeListener { handleMenuItemRegistrationEvent(payload.data); } else if (payload.type == "setTiles" && isSetTilesEvent(payload.data)) { this._setTilesStream.next(payload.data); - } else if (isSetVariableIframeEvent(payload)) { - this._setVariableStream.next(payload.data); - - // Let's dispatch the message to the other iframes - for (iframe of this.iframes) { - if (iframe.contentWindow !== message.source) { - iframe.contentWindow?.postMessage({ - 'type': 'setVariable', - 'data': payload.data - }, '*'); - } - } } } }, diff --git a/front/src/Api/iframe/room.ts b/front/src/Api/iframe/room.ts index 00d974dc..db639cd9 100644 --- a/front/src/Api/iframe/room.ts +++ b/front/src/Api/iframe/room.ts @@ -119,9 +119,9 @@ export class WorkadventureRoomCommands extends IframeApiContribution { variables.set(key, value); - sendToWorkadventure({ + return queryWorkadventure({ type: 'setVariable', data: { key, diff --git a/front/src/Phaser/Game/SharedVariablesManager.ts b/front/src/Phaser/Game/SharedVariablesManager.ts index aeb26d68..dfedcf80 100644 --- a/front/src/Phaser/Game/SharedVariablesManager.ts +++ b/front/src/Phaser/Game/SharedVariablesManager.ts @@ -14,7 +14,6 @@ interface Variable { export class SharedVariablesManager { private _variables = new Map(); - private iframeListenerSubscription: Subscription; private variableObjects: Map; constructor(private roomConnection: RoomConnection, private gameMap: GameMap) { @@ -28,7 +27,7 @@ export class SharedVariablesManager { } // When a variable is modified from an iFrame - this.iframeListenerSubscription = iframeListener.setVariableStream.subscribe((event) => { + iframeListener.registerAnswerer('setVariable', (event) => { const key = event.key; if (!this.variableObjects.has(key)) { @@ -75,7 +74,7 @@ export class SharedVariablesManager { } public close(): void { - this.iframeListenerSubscription.unsubscribe(); + iframeListener.unregisterAnswerer('setVariable'); } get variables(): Map { diff --git a/front/src/iframe_api.ts b/front/src/iframe_api.ts index fb44738f..da2e922e 100644 --- a/front/src/iframe_api.ts +++ b/front/src/iframe_api.ts @@ -192,7 +192,18 @@ window.addEventListener( console.debug(payload); - if (isIframeAnswerEvent(payload)) { + if (isIframeErrorAnswerEvent(payload)) { + const queryId = payload.id; + const payloadError = payload.error; + + const resolver = answerPromises.get(queryId); + if (resolver === undefined) { + throw new Error('In Iframe API, got an error answer for a question that we have no track of.'); + } + resolver.reject(new Error(payloadError)); + + answerPromises.delete(queryId); + } else if (isIframeAnswerEvent(payload)) { const queryId = payload.id; const payloadData = payload.data; @@ -202,17 +213,6 @@ window.addEventListener( } resolver.resolve(payloadData); - answerPromises.delete(queryId); - } else if (isIframeErrorAnswerEvent(payload)) { - const queryId = payload.id; - const payloadError = payload.error; - - const resolver = answerPromises.get(queryId); - if (resolver === undefined) { - throw new Error('In Iframe API, got an error answer for a question that we have no track of.'); - } - resolver.reject(payloadError); - answerPromises.delete(queryId); } else if (isIframeResponseEventWrapper(payload)) { const payloadData = payload.data; diff --git a/maps/tests/Variables/script.js b/maps/tests/Variables/script.js index cef9818e..ea381018 100644 --- a/maps/tests/Variables/script.js +++ b/maps/tests/Variables/script.js @@ -2,8 +2,10 @@ WA.onInit().then(() => { console.log('Trying to read variable "doorOpened" whose default property is true. This should display "true".'); console.log('doorOpened', WA.room.loadVariable('doorOpened')); - console.log('Trying to set variable "not_exists". This should display an error in the console.') - WA.room.saveVariable('not_exists', 'foo'); + console.log('Trying to set variable "not_exists". This should display an error in the console, followed by a log saying the error was caught.') + WA.room.saveVariable('not_exists', 'foo').catch((e) => { + console.log('Successfully caught error: ', e); + }); console.log('Trying to set variable "config". This should work.'); WA.room.saveVariable('config', {'foo': 'bar'}); From 0aa93543bc285d7c50ef4587b1eb51bb1da1147a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Mon, 5 Jul 2021 18:48:26 +0200 Subject: [PATCH 008/101] Adding warning if "template" object is used as a variable --- .../src/Phaser/Game/SharedVariablesManager.ts | 4 +++ front/src/Phaser/Map/ITiledMap.ts | 1 + front/src/iframe_api.ts | 2 +- maps/tests/Variables/variables.json | 36 +++++++++++++++++-- 4 files changed, 40 insertions(+), 3 deletions(-) diff --git a/front/src/Phaser/Game/SharedVariablesManager.ts b/front/src/Phaser/Game/SharedVariablesManager.ts index dfedcf80..283eb5c3 100644 --- a/front/src/Phaser/Game/SharedVariablesManager.ts +++ b/front/src/Phaser/Game/SharedVariablesManager.ts @@ -48,6 +48,10 @@ export class SharedVariablesManager { if (layer.type === 'objectgroup') { for (const object of layer.objects) { if (object.type === 'variable') { + if (object.template) { + console.warn('Warning, a variable object is using a Tiled "template". WorkAdventure does not support objects generated from Tiled templates.') + } + // We store a copy of the object (to make it immutable) objects.set(object.name, this.iTiledObjectToVariable(object)); } diff --git a/front/src/Phaser/Map/ITiledMap.ts b/front/src/Phaser/Map/ITiledMap.ts index c5b96f22..57bb13c9 100644 --- a/front/src/Phaser/Map/ITiledMap.ts +++ b/front/src/Phaser/Map/ITiledMap.ts @@ -141,6 +141,7 @@ export interface ITiledMapObject { polyline: { x: number; y: number }[]; text?: ITiledText; + template?: string; } export interface ITiledText { diff --git a/front/src/iframe_api.ts b/front/src/iframe_api.ts index da2e922e..cd610ab0 100644 --- a/front/src/iframe_api.ts +++ b/front/src/iframe_api.ts @@ -190,7 +190,7 @@ window.addEventListener( } const payload = message.data; - console.debug(payload); + //console.debug(payload); if (isIframeErrorAnswerEvent(payload)) { const queryId = payload.id; diff --git a/maps/tests/Variables/variables.json b/maps/tests/Variables/variables.json index 61067071..94d40560 100644 --- a/maps/tests/Variables/variables.json +++ b/maps/tests/Variables/variables.json @@ -68,8 +68,40 @@ "y":2.5 }, { + "height":0, "id":5, - "template":"config.tx", + "name":"config", + "point":true, + "properties":[ + { + "name":"default", + "type":"string", + "value":"{}" + }, + { + "name":"jsonSchema", + "type":"string", + "value":"{}" + }, + { + "name":"persist", + "type":"bool", + "value":true + }, + { + "name":"readableBy", + "type":"string", + "value":"" + }, + { + "name":"writableBy", + "type":"string", + "value":"admin" + }], + "rotation":0, + "type":"variable", + "visible":true, + "width":0, "x":57.5, "y":111 }, @@ -98,7 +130,7 @@ "y":0 }], "nextlayerid":8, - "nextobjectid":8, + "nextobjectid":9, "orientation":"orthogonal", "properties":[ { From 86fa869b20b0f1ce01247b5a37d6a5cb83318ae9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Tue, 6 Jul 2021 10:26:44 +0200 Subject: [PATCH 009/101] Actually using Type Guards in queries received by WA. --- front/src/Api/Events/IframeEvent.ts | 47 +++++++++++++++++++++++------ front/src/Api/IframeListener.ts | 10 +++--- maps/tests/Variables/variables.json | 20 +----------- 3 files changed, 45 insertions(+), 32 deletions(-) diff --git a/front/src/Api/Events/IframeEvent.ts b/front/src/Api/Events/IframeEvent.ts index 54319fd3..0d995255 100644 --- a/front/src/Api/Events/IframeEvent.ts +++ b/front/src/Api/Events/IframeEvent.ts @@ -1,3 +1,4 @@ +import * as tg from "generic-type-guard"; import type { GameStateEvent } from "./GameStateEvent"; import type { ButtonClickedEvent } from "./ButtonClickedEvent"; import type { ChatEvent } from "./ChatEvent"; @@ -19,6 +20,9 @@ import type { MenuItemRegisterEvent } from "./ui/MenuItemRegisterEvent"; import type { HasPlayerMovedEvent } from "./HasPlayerMovedEvent"; import type { SetTilesEvent } from "./SetTilesEvent"; import type { SetVariableEvent } from "./SetVariableEvent"; +import {isGameStateEvent} from "./GameStateEvent"; +import {isMapDataEvent} from "./MapDataEvent"; +import {isSetVariableEvent} from "./SetVariableEvent"; export interface TypedMessageEvent extends MessageEvent { data: T; @@ -81,20 +85,32 @@ export const isIframeResponseEventWrapper = (event: { /** - * List event types sent from an iFrame to WorkAdventure that expect a unique answer from WorkAdventure along the type for the answer from WorkAdventure to the iFrame + * List event types sent from an iFrame to WorkAdventure that expect a unique answer from WorkAdventure along the type for the answer from WorkAdventure to the iFrame. + * Types are defined using Type guards that will actually bused to enforce and check types. */ -export type IframeQueryMap = { +export const iframeQueryMapTypeGuards = { getState: { - query: undefined, - answer: GameStateEvent, + query: tg.isUndefined, + answer: isGameStateEvent, }, getMapData: { - query: undefined, - answer: MapDataEvent, + query: tg.isUndefined, + answer: isMapDataEvent, }, setVariable: { - query: SetVariableEvent, - answer: void + query: isSetVariableEvent, + answer: tg.isUndefined, + }, +} + +type GuardedType = T extends (x: unknown) => x is (infer T) ? T : never; +type IframeQueryMapTypeGuardsType = typeof iframeQueryMapTypeGuards; +type UnknownToVoid = undefined extends T ? void : T; + +export type IframeQueryMap = { + [key in keyof IframeQueryMapTypeGuardsType]: { + query: GuardedType + answer: UnknownToVoid> } } @@ -108,8 +124,21 @@ export interface IframeQueryWrapper { query: IframeQuery; } +export const isIframeQueryKey = (type: string): type is keyof IframeQueryMap => { + return type in iframeQueryMapTypeGuards; +} + // eslint-disable-next-line @typescript-eslint/no-explicit-any -export const isIframeQuery = (event: any): event is IframeQuery => typeof event.type === 'string'; +export const isIframeQuery = (event: any): event is IframeQuery => { + const type = event.type; + if (typeof type !== 'string') { + return false; + } + if (!isIframeQueryKey(type)) { + return false; + } + return iframeQueryMapTypeGuards[type].query(event.data); +} // eslint-disable-next-line @typescript-eslint/no-explicit-any export const isIframeQueryWrapper = (event: any): event is IframeQueryWrapper => typeof event.id === 'number' && isIframeQuery(event.query); diff --git a/front/src/Api/IframeListener.ts b/front/src/Api/IframeListener.ts index ee969721..9c61bdf8 100644 --- a/front/src/Api/IframeListener.ts +++ b/front/src/Api/IframeListener.ts @@ -168,13 +168,15 @@ class IframeListener { return; } - const errorHandler = (reason: any) => { + const errorHandler = (reason: unknown) => { console.error('An error occurred while responding to an iFrame query.', reason); - let reasonMsg: string; + let reasonMsg: string = ''; if (reason instanceof Error) { reasonMsg = reason.message; - } else { - reasonMsg = reason.toString(); + } else if (typeof reason === 'object') { + reasonMsg = reason ? reason.toString() : ''; + } else if (typeof reason === 'string') { + reasonMsg = reason; } iframe?.contentWindow?.postMessage({ diff --git a/maps/tests/Variables/variables.json b/maps/tests/Variables/variables.json index 94d40560..d74e90cc 100644 --- a/maps/tests/Variables/variables.json +++ b/maps/tests/Variables/variables.json @@ -14,24 +14,6 @@ "x":0, "y":0 }, - { - "data":[0, 0, 0, 0, 0, 0, 0, 0, 23, 23, 0, 0, 0, 0, 0, 0, 0, 0, 23, 23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - "height":10, - "id":6, - "name":"triggerZone", - "opacity":1, - "properties":[ - { - "name":"zone", - "type":"string", - "value":"myTrigger" - }], - "type":"tilelayer", - "visible":true, - "width":10, - "x":0, - "y":0 - }, { "data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], "height":10, @@ -58,7 +40,7 @@ { "fontfamily":"Sans Serif", "pixelsize":11, - "text":"Test:\nTODO", + "text":"Test:\nOpen your console\n\nResult:\nYou should see a list of tests performed and results associated.", "wrap":true }, "type":"", From cb78ff333bb62f2c896ea5f30ba60f1290c24f58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Tue, 6 Jul 2021 10:58:12 +0200 Subject: [PATCH 010/101] Adding client side check of setVariable with writableBy property --- front/src/Connexion/RoomConnection.ts | 13 +++++- .../src/Phaser/Game/SharedVariablesManager.ts | 38 ++++++++++++++++-- maps/tests/Variables/script.js | 13 ++++-- maps/tests/Variables/variables.json | 40 ++++++++++++++++++- messages/protos/messages.proto | 2 +- 5 files changed, 95 insertions(+), 11 deletions(-) diff --git a/front/src/Connexion/RoomConnection.ts b/front/src/Connexion/RoomConnection.ts index 1b080a55..c2d4157b 100644 --- a/front/src/Connexion/RoomConnection.ts +++ b/front/src/Connexion/RoomConnection.ts @@ -31,7 +31,7 @@ import { EmoteEventMessage, EmotePromptMessage, SendUserMessage, - BanUserMessage, + BanUserMessage, VariableMessage, } from "../Messages/generated/messages_pb"; import type { UserSimplePeerInterface } from "../WebRtc/SimplePeer"; @@ -536,6 +536,17 @@ export class RoomConnection implements RoomConnection { this.socket.send(clientToServerMessage.serializeBinary().buffer); } + emitSetVariableEvent(name: string, value: unknown): void { + const variableMessage = new VariableMessage(); + variableMessage.setName(name); + variableMessage.setValue(JSON.stringify(value)); + + const clientToServerMessage = new ClientToServerMessage(); + clientToServerMessage.setVariablemessage(variableMessage); + + this.socket.send(clientToServerMessage.serializeBinary().buffer); + } + onActionableEvent(callback: (message: ItemEventMessageInterface) => void): void { this.onMessage(EventMessage.ITEM_EVENT, (message: ItemEventMessage) => { callback({ diff --git a/front/src/Phaser/Game/SharedVariablesManager.ts b/front/src/Phaser/Game/SharedVariablesManager.ts index 283eb5c3..284dec1d 100644 --- a/front/src/Phaser/Game/SharedVariablesManager.ts +++ b/front/src/Phaser/Game/SharedVariablesManager.ts @@ -9,7 +9,9 @@ import type {ITile, ITiledMapObject} from "../Map/ITiledMap"; import type {Var} from "svelte/types/compiler/interfaces"; interface Variable { - defaultValue: unknown + defaultValue: unknown, + readableBy?: string, + writableBy?: string, } export class SharedVariablesManager { @@ -30,15 +32,24 @@ export class SharedVariablesManager { iframeListener.registerAnswerer('setVariable', (event) => { const key = event.key; - if (!this.variableObjects.has(key)) { + const object = this.variableObjects.get(key); + + if (object === undefined) { const errMsg = 'A script is trying to modify variable "'+key+'" but this variable is not defined in the map.' + 'There should be an object in the map whose name is "'+key+'" and whose type is "variable"'; console.error(errMsg); throw new Error(errMsg); } + if (object.writableBy && !this.roomConnection.hasTag(object.writableBy)) { + const errMsg = 'A script is trying to modify variable "'+key+'" but this variable is only writable for users with tag "'+object.writableBy+'".'; + console.error(errMsg); + throw new Error(errMsg); + } + this._variables.set(key, event.value); // TODO: dispatch to the room connection. + this.roomConnection.emitSetVariableEvent(key, event.value); }); } @@ -68,8 +79,27 @@ export class SharedVariablesManager { if (object.properties) { for (const property of object.properties) { - if (property.name === 'default') { - variable.defaultValue = property.value; + const value = property.value; + switch (property.name) { + case 'default': + variable.defaultValue = value; + break; + case 'writableBy': + if (typeof value !== 'string') { + throw new Error('The writableBy property of variable "'+object.name+'" must be a string'); + } + if (value) { + variable.writableBy = value; + } + break; + case 'readableBy': + if (typeof value !== 'string') { + throw new Error('The readableBy property of variable "'+object.name+'" must be a string'); + } + if (value) { + variable.readableBy = value; + } + break; } } } diff --git a/maps/tests/Variables/script.js b/maps/tests/Variables/script.js index ea381018..120a4425 100644 --- a/maps/tests/Variables/script.js +++ b/maps/tests/Variables/script.js @@ -7,9 +7,14 @@ WA.onInit().then(() => { console.log('Successfully caught error: ', e); }); - console.log('Trying to set variable "config". This should work.'); - WA.room.saveVariable('config', {'foo': 'bar'}); + console.log('Trying to set variable "myvar". This should work.'); + WA.room.saveVariable('myvar', {'foo': 'bar'}); - console.log('Trying to read variable "config". This should display a {"foo": "bar"} object.'); - console.log(WA.room.loadVariable('config')); + console.log('Trying to read variable "myvar". This should display a {"foo": "bar"} object.'); + console.log(WA.room.loadVariable('myvar')); + + console.log('Trying to set variable "config". This should not work because we are not logged as admin.'); + WA.room.saveVariable('config', {'foo': 'bar'}).catch(e => { + console.log('Successfully caught error because variable "config" is not writable: ', e); + }); }); diff --git a/maps/tests/Variables/variables.json b/maps/tests/Variables/variables.json index d74e90cc..79ca591b 100644 --- a/maps/tests/Variables/variables.json +++ b/maps/tests/Variables/variables.json @@ -104,6 +104,44 @@ "width":0, "x":131.38069962269, "y":106.004988169086 + }, + { + "height":0, + "id":9, + "name":"myvar", + "point":true, + "properties":[ + { + "name":"default", + "type":"string", + "value":"{}" + }, + { + "name":"jsonSchema", + "type":"string", + "value":"{}" + }, + { + "name":"persist", + "type":"bool", + "value":true + }, + { + "name":"readableBy", + "type":"string", + "value":"" + }, + { + "name":"writableBy", + "type":"string", + "value":"" + }], + "rotation":0, + "type":"variable", + "visible":true, + "width":0, + "x":88.8149900876127, + "y":147.75212636695 }], "opacity":1, "type":"objectgroup", @@ -112,7 +150,7 @@ "y":0 }], "nextlayerid":8, - "nextobjectid":9, + "nextobjectid":10, "orientation":"orthogonal", "properties":[ { diff --git a/messages/protos/messages.proto b/messages/protos/messages.proto index a9483dd9..30882cd9 100644 --- a/messages/protos/messages.proto +++ b/messages/protos/messages.proto @@ -186,7 +186,7 @@ message RoomJoinedMessage { repeated ItemStateMessage item = 3; int32 currentUserId = 4; repeated string tag = 5; - repeated VariableMessage = 6; + repeated VariableMessage variable = 6; } message WebRtcStartMessage { From a1f1927b6d94f03ef97fc070b22016a30ea8302c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Tue, 6 Jul 2021 15:30:49 +0200 Subject: [PATCH 011/101] Starting adding variables server-side --- back/src/Model/GameRoom.ts | 7 +- back/src/RoomManager.ts | 4 +- back/src/Services/SocketManager.ts | 26 ++++++- messages/protos/messages.proto | 24 ++++++- pusher/src/Controller/IoSocketController.ts | 4 +- pusher/src/Model/PusherRoom.ts | 79 +++++++++++++++++++-- pusher/src/Services/SocketManager.ts | 33 ++++----- 7 files changed, 147 insertions(+), 30 deletions(-) diff --git a/back/src/Model/GameRoom.ts b/back/src/Model/GameRoom.ts index 020f4c29..33af483f 100644 --- a/back/src/Model/GameRoom.ts +++ b/back/src/Model/GameRoom.ts @@ -34,7 +34,8 @@ export class GameRoom { private readonly connectCallback: ConnectCallback; private readonly disconnectCallback: DisconnectCallback; - private itemsState: Map = new Map(); + private itemsState = new Map(); + private variables = new Map(); private readonly positionNotifier: PositionNotifier; public readonly roomId: string; @@ -309,6 +310,10 @@ export class GameRoom { return this.itemsState; } + public setVariable(name: string, value: string): void { + this.variables.set(name, value); + } + public addZoneListener(call: ZoneSocket, x: number, y: number): Set { return this.positionNotifier.addZoneListener(call, x, y); } diff --git a/back/src/RoomManager.ts b/back/src/RoomManager.ts index 9aaf1edb..2514c576 100644 --- a/back/src/RoomManager.ts +++ b/back/src/RoomManager.ts @@ -16,7 +16,7 @@ import { ServerToAdminClientMessage, ServerToClientMessage, SilentMessage, - UserMovesMessage, + UserMovesMessage, VariableMessage, WebRtcSignalToServerMessage, WorldFullWarningToRoomMessage, ZoneMessage, @@ -72,6 +72,8 @@ const roomManager: IRoomManagerServer = { socketManager.handleSilentMessage(room, user, message.getSilentmessage() as SilentMessage); } else if (message.hasItemeventmessage()) { socketManager.handleItemEvent(room, user, message.getItemeventmessage() as ItemEventMessage); + } else if (message.hasVariablemessage()) { + socketManager.handleVariableEvent(room, user, message.getVariablemessage() as VariableMessage); } else if (message.hasWebrtcsignaltoservermessage()) { socketManager.emitVideo( room, diff --git a/back/src/Services/SocketManager.ts b/back/src/Services/SocketManager.ts index e61763cd..e8356245 100644 --- a/back/src/Services/SocketManager.ts +++ b/back/src/Services/SocketManager.ts @@ -29,7 +29,7 @@ import { EmoteEventMessage, BanUserMessage, RefreshRoomMessage, - EmotePromptMessage, + EmotePromptMessage, VariableMessage, } from "../Messages/generated/messages_pb"; import { User, UserSocket } from "../Model/User"; import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils"; @@ -184,6 +184,28 @@ export class SocketManager { } } + handleVariableEvent(room: GameRoom, user: User, variableMessage: VariableMessage) { + const itemEvent = ProtobufUtils.toItemEvent(itemEventMessage); + + try { + // TODO: DISPATCH ON NEW ROOM CHANNEL + + const subMessage = new SubMessage(); + subMessage.setItemeventmessage(itemEventMessage); + + // Let's send the event without using the SocketIO room. + // TODO: move this in the GameRoom class. + for (const user of room.getUsers().values()) { + user.emitInBatch(subMessage); + } + + room.setVariable(variableMessage.getName(), variableMessage.getValue()); + } catch (e) { + console.error('An error occurred on "item_event"'); + console.error(e); + } + } + emitVideo(room: GameRoom, user: User, data: WebRtcSignalToServerMessage): void { //send only at user const remoteUser = room.getUsers().get(data.getReceiverid()); @@ -425,6 +447,7 @@ export class SocketManager { // Let's send 2 messages: one to the user joining the group and one to the other user const webrtcStartMessage1 = new WebRtcStartMessage(); webrtcStartMessage1.setUserid(otherUser.id); + webrtcStartMessage1.setUseruuid(otherUser.uuid); webrtcStartMessage1.setName(otherUser.name); webrtcStartMessage1.setInitiator(true); if (TURN_STATIC_AUTH_SECRET !== "") { @@ -443,6 +466,7 @@ export class SocketManager { const webrtcStartMessage2 = new WebRtcStartMessage(); webrtcStartMessage2.setUserid(user.id); + webrtcStartMessage2.setUseruuid(user.uuid); webrtcStartMessage2.setName(user.name); webrtcStartMessage2.setInitiator(false); if (TURN_STATIC_AUTH_SECRET !== "") { diff --git a/messages/protos/messages.proto b/messages/protos/messages.proto index 30882cd9..289c0724 100644 --- a/messages/protos/messages.proto +++ b/messages/protos/messages.proto @@ -325,6 +325,10 @@ message ZoneMessage { int32 y = 3; } +message RoomMessage { + string roomId = 1; +} + message PusherToBackMessage { oneof message { JoinRoomMessage joinRoomMessage = 1; @@ -360,10 +364,20 @@ message SubToPusherMessage { SendUserMessage sendUserMessage = 7; BanUserMessage banUserMessage = 8; EmoteEventMessage emoteEventMessage = 9; - VariableMessage variableMessage = 10; } } +message BatchToPusherRoomMessage { + repeated SubToPusherRoomMessage payload = 2; +} + +message SubToPusherRoomMessage { + oneof message { + VariableMessage variableMessage = 1; + } +} + + /*message BatchToAdminPusherMessage { repeated SubToAdminPusherMessage payload = 2; }*/ @@ -433,9 +447,13 @@ message EmptyMessage { } +/** + * Service handled by the "back". Pusher servers connect to this service. + */ service RoomManager { - rpc joinRoom(stream PusherToBackMessage) returns (stream ServerToClientMessage); - rpc listenZone(ZoneMessage) returns (stream BatchToPusherMessage); + rpc joinRoom(stream PusherToBackMessage) returns (stream ServerToClientMessage); // Holds a connection between one given client and the back + rpc listenZone(ZoneMessage) returns (stream BatchToPusherMessage); // Connection used to send to a pusher messages related to a given zone of a given room + rpc listenRoom(RoomMessage) returns (stream BatchToPusherRoomMessage); // Connection used to send to a pusher messages related to a given room rpc adminRoom(stream AdminPusherToBackMessage) returns (stream ServerToAdminClientMessage); rpc sendAdminMessage(AdminMessage) returns (EmptyMessage); rpc sendGlobalAdminMessage(AdminGlobalMessage) returns (EmptyMessage); diff --git a/pusher/src/Controller/IoSocketController.ts b/pusher/src/Controller/IoSocketController.ts index 1af9d917..a6fddb34 100644 --- a/pusher/src/Controller/IoSocketController.ts +++ b/pusher/src/Controller/IoSocketController.ts @@ -16,7 +16,7 @@ import { SendUserMessage, ServerToClientMessage, CompanionMessage, - EmotePromptMessage, + EmotePromptMessage, VariableMessage, } from "../Messages/generated/messages_pb"; import { UserMovesMessage } from "../Messages/generated/messages_pb"; import { TemplatedApp } from "uWebSockets.js"; @@ -358,6 +358,8 @@ export class IoSocketController { socketManager.handleSilentMessage(client, message.getSilentmessage() as SilentMessage); } else if (message.hasItemeventmessage()) { socketManager.handleItemEvent(client, message.getItemeventmessage() as ItemEventMessage); + } else if (message.hasVariablemessage()) { + socketManager.handleVariableEvent(client, message.getVariablemessage() as VariableMessage); } else if (message.hasWebrtcsignaltoservermessage()) { socketManager.emitVideo( client, diff --git a/pusher/src/Model/PusherRoom.ts b/pusher/src/Model/PusherRoom.ts index a49fce3e..1eae7a9f 100644 --- a/pusher/src/Model/PusherRoom.ts +++ b/pusher/src/Model/PusherRoom.ts @@ -3,10 +3,21 @@ import { PositionDispatcher } from "./PositionDispatcher"; import { ViewportInterface } from "_Model/Websocket/ViewportMessage"; import { extractDataFromPrivateRoomId, extractRoomSlugPublicRoomId, isRoomAnonymous } from "./RoomIdentifier"; import { arrayIntersect } from "../Services/ArrayHelper"; -import { ZoneEventListener } from "_Model/Zone"; +import {GroupDescriptor, UserDescriptor, ZoneEventListener} from "_Model/Zone"; +import {apiClientRepository} from "../Services/ApiClientRepository"; +import { + BatchToPusherMessage, BatchToPusherRoomMessage, EmoteEventMessage, GroupLeftZoneMessage, + GroupUpdateZoneMessage, RoomMessage, SubMessage, + UserJoinedZoneMessage, UserLeftZoneMessage, UserMovedMessage, VariableMessage, + ZoneMessage +} from "../Messages/generated/messages_pb"; +import Debug from "debug"; +import {ClientReadableStream} from "grpc"; + +const debug = Debug("room"); export enum GameRoomPolicyTypes { - ANONYMUS_POLICY = 1, + ANONYMOUS_POLICY = 1, MEMBERS_ONLY_POLICY, USE_TAGS_POLICY, } @@ -20,11 +31,14 @@ export class PusherRoom { public readonly worldSlug: string = ""; public readonly organizationSlug: string = ""; private versionNumber: number = 1; + private backConnection!: ClientReadableStream; + private isClosing: boolean = false; + private listeners: Set = new Set(); - constructor(public readonly roomId: string, private socketListener: ZoneEventListener) { + constructor(public readonly roomId: string, private socketListener: ZoneEventListener, private onBackFailure: (e: Error | null, room: PusherRoom) => void) { this.public = isRoomAnonymous(roomId); this.tags = []; - this.policyType = GameRoomPolicyTypes.ANONYMUS_POLICY; + this.policyType = GameRoomPolicyTypes.ANONYMOUS_POLICY; if (this.public) { this.roomSlug = extractRoomSlugPublicRoomId(this.roomId); @@ -43,8 +57,13 @@ export class PusherRoom { this.positionNotifier.setViewport(socket, viewport); } + public join(socket: ExSocketInterface) { + this.listeners.add(socket); + } + public leave(socket: ExSocketInterface) { this.positionNotifier.removeViewport(socket); + this.listeners.delete(socket); } public canAccess(userTags: string[]): boolean { @@ -63,4 +82,56 @@ export class PusherRoom { return false; } } + + /** + * Creates a connection to the back server to track global messages relative to this room (like variable changes). + */ + public async init(): Promise { + debug("Opening connection to room %s on back server", this.roomId); + const apiClient = await apiClientRepository.getClient(this.roomId); + const roomMessage = new RoomMessage(); + roomMessage.setRoomid(this.roomId); + this.backConnection = apiClient.listenRoom(roomMessage); + this.backConnection.on("data", (batch: BatchToPusherRoomMessage) => { + for (const message of batch.getPayloadList()) { + if (message.hasVariablemessage()) { + const variableMessage = message.getVariablemessage() as VariableMessage; + // We need to dispatch this variable to all the listeners + for (const listener of this.listeners) { + const subMessage = new SubMessage(); + subMessage.setVariablemessage(variableMessage); + listener.emitInBatch(subMessage); + } + } else { + throw new Error("Unexpected message"); + } + } + }); + + this.backConnection.on("error", (e) => { + if (!this.isClosing) { + debug("Error on back connection"); + this.close(); + this.onBackFailure(e, this); + } + }); + this.backConnection.on("close", () => { + if (!this.isClosing) { + debug("Close on back connection"); + this.close(); + this.onBackFailure(null, this); + } + }); + } + + public close(): void { + debug("Closing connection to room %s on back server", this.roomId); + this.isClosing = true; + this.backConnection.cancel(); + + // Let's close all connections linked to that room + for (const listener of this.listeners) { + listener.close(); + } + } } diff --git a/pusher/src/Services/SocketManager.ts b/pusher/src/Services/SocketManager.ts index 8a0d3673..6c78d398 100644 --- a/pusher/src/Services/SocketManager.ts +++ b/pusher/src/Services/SocketManager.ts @@ -29,7 +29,7 @@ import { AdminMessage, BanMessage, RefreshRoomMessage, - EmotePromptMessage, + EmotePromptMessage, VariableMessage, } from "../Messages/generated/messages_pb"; import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils"; import { JITSI_ISS, SECRET_JITSI_KEY } from "../Enum/EnvironmentVariable"; @@ -227,6 +227,9 @@ export class SocketManager implements ZoneEventListener { const pusherToBackMessage = new PusherToBackMessage(); pusherToBackMessage.setJoinroommessage(joinRoomMessage); streamToPusher.write(pusherToBackMessage); + + const pusherRoom = await this.getOrCreateRoom(client.roomId); + pusherRoom.join(client); } catch (e) { console.error('An error occurred on "join_room" event'); console.error(e); @@ -300,6 +303,13 @@ export class SocketManager implements ZoneEventListener { client.backConnection.write(pusherToBackMessage); } + handleVariableEvent(client: ExSocketInterface, variableMessage: VariableMessage) { + const pusherToBackMessage = new PusherToBackMessage(); + pusherToBackMessage.setVariablemessage(variableMessage); + + client.backConnection.write(pusherToBackMessage); + } + async handleReportMessage(client: ExSocketInterface, reportPlayerMessage: ReportPlayerMessage) { try { const reportedSocket = this.sockets.get(reportPlayerMessage.getReporteduserid()); @@ -334,14 +344,6 @@ export class SocketManager implements ZoneEventListener { socket.backConnection.write(pusherToBackMessage); } - private searchClientByIdOrFail(userId: number): ExSocketInterface { - const client: ExSocketInterface | undefined = this.sockets.get(userId); - if (client === undefined) { - throw new Error("Could not find user with id " + userId); - } - return client; - } - leaveRoom(socket: ExSocketInterface) { // leave previous room and world try { @@ -354,6 +356,7 @@ export class SocketManager implements ZoneEventListener { room.leave(socket); if (room.isEmpty()) { + room.close(); this.rooms.delete(socket.roomId); debug("Room %s is empty. Deleting.", socket.roomId); } @@ -384,9 +387,10 @@ export class SocketManager implements ZoneEventListener { if (!world.public) { await this.updateRoomWithAdminData(world); } + await world.init(); this.rooms.set(roomId, world); } - return Promise.resolve(world); + return world; } public async updateRoomWithAdminData(world: PusherRoom): Promise { @@ -410,15 +414,6 @@ export class SocketManager implements ZoneEventListener { return this.rooms; } - searchClientByUuid(uuid: string): ExSocketInterface | null { - for (const socket of this.sockets.values()) { - if (socket.userUuid === uuid) { - return socket; - } - } - return null; - } - public handleQueryJitsiJwtMessage(client: ExSocketInterface, queryJitsiJwtMessage: QueryJitsiJwtMessage) { try { const room = queryJitsiJwtMessage.getJitsiroom(); From 46e6917df6a26969d3a7e7a4ec7862bee61aa39d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Tue, 6 Jul 2021 17:13:08 +0200 Subject: [PATCH 012/101] Adding a playersStore The playerStore can be useful to get the details of a given player from its ID. --- back/src/Services/SocketManager.ts | 2 - front/src/Connexion/RoomConnection.ts | 1 - front/src/Phaser/Game/AddPlayerInterface.ts | 9 +---- front/src/Phaser/Game/GameScene.ts | 3 ++ front/src/Phaser/Game/PlayerInterface.ts | 9 +++++ front/src/Stores/PlayersStore.ts | 43 +++++++++++++++++++++ front/src/WebRtc/SimplePeer.ts | 16 ++------ messages/protos/messages.proto | 3 +- 8 files changed, 62 insertions(+), 24 deletions(-) create mode 100644 front/src/Phaser/Game/PlayerInterface.ts create mode 100644 front/src/Stores/PlayersStore.ts diff --git a/back/src/Services/SocketManager.ts b/back/src/Services/SocketManager.ts index e61763cd..fd812b44 100644 --- a/back/src/Services/SocketManager.ts +++ b/back/src/Services/SocketManager.ts @@ -425,7 +425,6 @@ export class SocketManager { // Let's send 2 messages: one to the user joining the group and one to the other user const webrtcStartMessage1 = new WebRtcStartMessage(); webrtcStartMessage1.setUserid(otherUser.id); - webrtcStartMessage1.setName(otherUser.name); webrtcStartMessage1.setInitiator(true); if (TURN_STATIC_AUTH_SECRET !== "") { const { username, password } = this.getTURNCredentials("" + otherUser.id, TURN_STATIC_AUTH_SECRET); @@ -443,7 +442,6 @@ export class SocketManager { const webrtcStartMessage2 = new WebRtcStartMessage(); webrtcStartMessage2.setUserid(user.id); - webrtcStartMessage2.setName(user.name); webrtcStartMessage2.setInitiator(false); if (TURN_STATIC_AUTH_SECRET !== "") { const { username, password } = this.getTURNCredentials("" + user.id, TURN_STATIC_AUTH_SECRET); diff --git a/front/src/Connexion/RoomConnection.ts b/front/src/Connexion/RoomConnection.ts index 1b080a55..4f2e9ef4 100644 --- a/front/src/Connexion/RoomConnection.ts +++ b/front/src/Connexion/RoomConnection.ts @@ -466,7 +466,6 @@ export class RoomConnection implements RoomConnection { this.onMessage(EventMessage.WEBRTC_START, (message: WebRtcStartMessage) => { callback({ userId: message.getUserid(), - name: message.getName(), initiator: message.getInitiator(), webRtcUser: message.getWebrtcusername() ?? undefined, webRtcPassword: message.getWebrtcpassword() ?? undefined, diff --git a/front/src/Phaser/Game/AddPlayerInterface.ts b/front/src/Phaser/Game/AddPlayerInterface.ts index 1a5176f0..d2f12013 100644 --- a/front/src/Phaser/Game/AddPlayerInterface.ts +++ b/front/src/Phaser/Game/AddPlayerInterface.ts @@ -1,11 +1,6 @@ import type {PointInterface} from "../../Connexion/ConnexionModels"; -import type {BodyResourceDescriptionInterface} from "../Entity/PlayerTextures"; +import type {PlayerInterface} from "./PlayerInterface"; -export interface AddPlayerInterface { - userId: number; - name: string; - characterLayers: BodyResourceDescriptionInterface[]; +export interface AddPlayerInterface extends PlayerInterface { position: PointInterface; - visitCardUrl: string|null; - companion: string|null; } diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index d767f0f4..ce9ce5e4 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -91,6 +91,7 @@ import { soundManager } from "./SoundManager"; import { peerStore, screenSharingPeerStore } from "../../Stores/PeerStore"; import { videoFocusStore } from "../../Stores/VideoFocusStore"; import { biggestAvailableAreaStore } from "../../Stores/BiggestAvailableAreaStore"; +import {playersStore} from "../../Stores/PlayersStore"; export interface GameSceneInitInterface { initPosition: PointInterface | null; @@ -597,6 +598,8 @@ export class GameScene extends DirtyScene { .then((onConnect: OnConnectInterface) => { this.connection = onConnect.connection; + playersStore.connectToRoomConnection(this.connection); + this.connection.onUserJoins((message: MessageUserJoined) => { const userMessage: AddPlayerInterface = { userId: message.userId, diff --git a/front/src/Phaser/Game/PlayerInterface.ts b/front/src/Phaser/Game/PlayerInterface.ts new file mode 100644 index 00000000..ab881267 --- /dev/null +++ b/front/src/Phaser/Game/PlayerInterface.ts @@ -0,0 +1,9 @@ +import type {BodyResourceDescriptionInterface} from "../Entity/PlayerTextures"; + +export interface PlayerInterface { + userId: number; + name: string; + characterLayers: BodyResourceDescriptionInterface[]; + visitCardUrl: string|null; + companion: string|null; +} diff --git a/front/src/Stores/PlayersStore.ts b/front/src/Stores/PlayersStore.ts new file mode 100644 index 00000000..ef19efce --- /dev/null +++ b/front/src/Stores/PlayersStore.ts @@ -0,0 +1,43 @@ +import { writable } from "svelte/store"; +import type {PlayerInterface} from "../Phaser/Game/PlayerInterface"; +import type {RoomConnection} from "../Connexion/RoomConnection"; + +/** + * A store that contains the list of players currently known. + */ +function createPlayersStore() { + let players = new Map(); + + const { subscribe, set, update } = writable(players); + + return { + subscribe, + connectToRoomConnection: (roomConnection: RoomConnection) => { + players = new Map(); + set(players); + roomConnection.onUserJoins((message) => { + update((users) => { + users.set(message.userId, { + userId: message.userId, + name: message.name, + characterLayers: message.characterLayers, + visitCardUrl: message.visitCardUrl, + companion: message.companion, + }); + return users; + }); + }); + roomConnection.onUserLeft((userId) => { + update((users) => { + users.delete(userId); + return users; + }); + }); + }, + getPlayerById(userId: number): PlayerInterface|undefined { + return players.get(userId); + } + }; +} + +export const playersStore = createPlayersStore(); diff --git a/front/src/WebRtc/SimplePeer.ts b/front/src/WebRtc/SimplePeer.ts index affcacd7..0d3c4745 100644 --- a/front/src/WebRtc/SimplePeer.ts +++ b/front/src/WebRtc/SimplePeer.ts @@ -11,10 +11,10 @@ import { get } from "svelte/store"; import { localStreamStore, LocalStreamStoreValue, obtainedMediaConstraintStore } from "../Stores/MediaStore"; import { screenSharingLocalStreamStore } from "../Stores/ScreenSharingStore"; import { discussionManager } from "./DiscussionManager"; +import {playersStore} from "../Stores/PlayersStore"; export interface UserSimplePeerInterface { userId: number; - name?: string; initiator?: boolean; webRtcUser?: string | undefined; webRtcPassword?: string | undefined; @@ -153,10 +153,7 @@ export class SimplePeer { } } - let name = user.name; - if (!name) { - name = this.getName(user.userId); - } + const name = this.getName(user.userId); discussionManager.removeParticipant(user.userId); @@ -191,7 +188,7 @@ export class SimplePeer { //Create a notification for first user in circle discussion if (this.PeerConnectionArray.size === 0) { - mediaManager.createNotification(user.name ?? ""); + mediaManager.createNotification(name); } this.PeerConnectionArray.set(user.userId, peer); @@ -202,12 +199,7 @@ export class SimplePeer { } private getName(userId: number): string { - const userSearch = this.Users.find((userSearch: UserSimplePeerInterface) => userSearch.userId === userId); - if (userSearch) { - return userSearch.name || ""; - } else { - return ""; - } + return playersStore.getPlayerById(userId)?.name || ''; } /** diff --git a/messages/protos/messages.proto b/messages/protos/messages.proto index 52d58d6d..27d7cb10 100644 --- a/messages/protos/messages.proto +++ b/messages/protos/messages.proto @@ -183,7 +183,6 @@ message RoomJoinedMessage { message WebRtcStartMessage { int32 userId = 1; - string name = 2; bool initiator = 3; string webrtcUserName = 4; string webrtcPassword = 5; @@ -257,7 +256,7 @@ message ServerToClientMessage { AdminRoomMessage adminRoomMessage = 14; WorldFullWarningMessage worldFullWarningMessage = 15; WorldFullMessage worldFullMessage = 16; - RefreshRoomMessage refreshRoomMessage = 17; + RefreshRoomMessage refreshRoomMessage = 17; WorldConnexionMessage worldConnexionMessage = 18; EmoteEventMessage emoteEventMessage = 19; } From 34cb0ebf39e2d21acc5ed9ddb6113259db85e7eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Wed, 7 Jul 2021 11:24:51 +0200 Subject: [PATCH 013/101] Users blocking now rely on UUID rather than ID This way, if a user A blocks another user B, if user B refreshes the browser or leaves and re-enters the room, user B will still be blocked. As a side effect, this allows us to completely remove the "sockets" property in the SocketManager on the Pusher. --- CHANGELOG.md | 1 + back/src/Services/SocketManager.ts | 2 + front/src/Connexion/ConnexionModels.ts | 68 +++++++++++---------- front/src/Connexion/RoomConnection.ts | 5 +- front/src/Phaser/Game/GameScene.ts | 7 ++- front/src/Phaser/Game/PlayerInterface.ts | 7 ++- front/src/Phaser/Menu/MenuScene.ts | 7 ++- front/src/Phaser/Menu/ReportMenu.ts | 78 ++++++++++++------------ front/src/Stores/PlayersStore.ts | 9 +-- front/src/WebRtc/BlackListManager.ts | 37 +++++------ front/src/WebRtc/SimplePeer.ts | 10 +-- front/src/WebRtc/VideoPeer.ts | 15 +++-- messages/protos/messages.proto | 4 +- pusher/src/Model/Zone.ts | 3 + pusher/src/Services/SocketManager.ts | 31 +--------- 15 files changed, 143 insertions(+), 141 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fa3dd293..a83e8213 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ - Use `WA.room.getCurrentRoom(): Promise` to get the ID, JSON map file, url of the map of the current room and the layer where the current player started - Use `WA.ui.registerMenuCommand(): void` to add a custom menu - Use `WA.room.setTiles(): void` to change an array of tiles +- Users blocking now relies on UUID rather than ID. A blocked user that leaves a room and comes back will stay blocked. ## Version 1.4.3 - 1.4.4 - 1.4.5 diff --git a/back/src/Services/SocketManager.ts b/back/src/Services/SocketManager.ts index fd812b44..8d04e713 100644 --- a/back/src/Services/SocketManager.ts +++ b/back/src/Services/SocketManager.ts @@ -308,6 +308,7 @@ export class SocketManager { throw new Error("clientUser.userId is not an integer " + thing.id); } userJoinedZoneMessage.setUserid(thing.id); + userJoinedZoneMessage.setUseruuid(thing.uuid); userJoinedZoneMessage.setName(thing.name); userJoinedZoneMessage.setCharacterlayersList(ProtobufUtils.toCharacterLayerMessages(thing.characterLayers)); userJoinedZoneMessage.setPosition(ProtobufUtils.toPositionMessage(thing.getPosition())); @@ -612,6 +613,7 @@ export class SocketManager { if (thing instanceof User) { const userJoinedMessage = new UserJoinedZoneMessage(); userJoinedMessage.setUserid(thing.id); + userJoinedMessage.setUseruuid(thing.uuid); userJoinedMessage.setName(thing.name); userJoinedMessage.setCharacterlayersList(ProtobufUtils.toCharacterLayerMessages(thing.characterLayers)); userJoinedMessage.setPosition(ProtobufUtils.toPositionMessage(thing.getPosition())); diff --git a/front/src/Connexion/ConnexionModels.ts b/front/src/Connexion/ConnexionModels.ts index b5a66296..189aea7c 100644 --- a/front/src/Connexion/ConnexionModels.ts +++ b/front/src/Connexion/ConnexionModels.ts @@ -1,8 +1,8 @@ -import type {SignalData} from "simple-peer"; -import type {RoomConnection} from "./RoomConnection"; -import type {BodyResourceDescriptionInterface} from "../Phaser/Entity/PlayerTextures"; +import type { SignalData } from "simple-peer"; +import type { RoomConnection } from "./RoomConnection"; +import type { BodyResourceDescriptionInterface } from "../Phaser/Entity/PlayerTextures"; -export enum EventMessage{ +export enum EventMessage { CONNECT = "connect", WEBRTC_SIGNAL = "webrtc-signal", WEBRTC_SCREEN_SHARING_SIGNAL = "webrtc-screen-sharing-signal", @@ -17,7 +17,7 @@ export enum EventMessage{ GROUP_CREATE_UPDATE = "group-create-update", GROUP_DELETE = "group-delete", SET_PLAYER_DETAILS = "set-player-details", // Send the name and character to the server (on connect), receive back the id. - ITEM_EVENT = 'item-event', + ITEM_EVENT = "item-event", CONNECT_ERROR = "connect_error", CONNECTING_ERROR = "connecting_error", @@ -36,7 +36,7 @@ export enum EventMessage{ export interface PointInterface { x: number; y: number; - direction : string; + direction: string; moving: boolean; } @@ -45,8 +45,9 @@ export interface MessageUserPositionInterface { name: string; characterLayers: BodyResourceDescriptionInterface[]; position: PointInterface; - visitCardUrl: string|null; - companion: string|null; + visitCardUrl: string | null; + companion: string | null; + userUuid: string; } export interface MessageUserMovedInterface { @@ -60,58 +61,59 @@ export interface MessageUserJoined { characterLayers: BodyResourceDescriptionInterface[]; position: PointInterface; visitCardUrl: string | null; - companion: string|null; + companion: string | null; + userUuid: string; } export interface PositionInterface { - x: number, - y: number + x: number; + y: number; } export interface GroupCreatedUpdatedMessageInterface { - position: PositionInterface, - groupId: number, - groupSize: number + position: PositionInterface; + groupId: number; + groupSize: number; } export interface WebRtcDisconnectMessageInterface { - userId: number + userId: number; } export interface WebRtcSignalReceivedMessageInterface { - userId: number, - signal: SignalData, - webRtcUser: string | undefined, - webRtcPassword: string | undefined + userId: number; + signal: SignalData; + webRtcUser: string | undefined; + webRtcPassword: string | undefined; } export interface ViewportInterface { - left: number, - top: number, - right: number, - bottom: number, + left: number; + top: number; + right: number; + bottom: number; } export interface ItemEventMessageInterface { - itemId: number, - event: string, - state: unknown, - parameters: unknown + itemId: number; + event: string; + state: unknown; + parameters: unknown; } export interface RoomJoinedMessageInterface { //users: MessageUserPositionInterface[], //groups: GroupCreatedUpdatedMessageInterface[], - items: { [itemId: number] : unknown } + items: { [itemId: number]: unknown }; } export interface PlayGlobalMessageInterface { - id: string - type: string - message: string + id: string; + type: string; + message: string; } export interface OnConnectInterface { - connection: RoomConnection, - room: RoomJoinedMessageInterface + connection: RoomConnection; + room: RoomJoinedMessageInterface; } diff --git a/front/src/Connexion/RoomConnection.ts b/front/src/Connexion/RoomConnection.ts index 4f2e9ef4..189eabba 100644 --- a/front/src/Connexion/RoomConnection.ts +++ b/front/src/Connexion/RoomConnection.ts @@ -365,6 +365,7 @@ export class RoomConnection implements RoomConnection { visitCardUrl: message.getVisitcardurl(), position: ProtobufClientUtils.toPointInterface(position), companion: companion ? companion.getName() : null, + userUuid: message.getUseruuid(), }; } @@ -591,9 +592,9 @@ export class RoomConnection implements RoomConnection { this.socket.send(clientToServerMessage.serializeBinary().buffer); } - public emitReportPlayerMessage(reportedUserId: number, reportComment: string): void { + public emitReportPlayerMessage(reportedUserUuid: string, reportComment: string): void { const reportPlayerMessage = new ReportPlayerMessage(); - reportPlayerMessage.setReporteduserid(reportedUserId); + reportPlayerMessage.setReporteduseruuid(reportedUserUuid); reportPlayerMessage.setReportcomment(reportComment); const clientToServerMessage = new ClientToServerMessage(); diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index ce9ce5e4..d6df242f 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -91,7 +91,7 @@ import { soundManager } from "./SoundManager"; import { peerStore, screenSharingPeerStore } from "../../Stores/PeerStore"; import { videoFocusStore } from "../../Stores/VideoFocusStore"; import { biggestAvailableAreaStore } from "../../Stores/BiggestAvailableAreaStore"; -import {playersStore} from "../../Stores/PlayersStore"; +import { playersStore } from "../../Stores/PlayersStore"; export interface GameSceneInitInterface { initPosition: PointInterface | null; @@ -608,6 +608,7 @@ export class GameScene extends DirtyScene { position: message.position, visitCardUrl: message.visitCardUrl, companion: message.companion, + userUuid: message.userUuid, }; this.addPlayer(userMessage); }); @@ -1047,7 +1048,7 @@ ${escapedMessage} }) ); - iframeListener.registerAnswerer('getState', () => { + iframeListener.registerAnswerer("getState", () => { return { mapUrl: this.MapUrlFile, startLayerName: this.startPositionCalculator.startLayerName, @@ -1150,7 +1151,7 @@ ${escapedMessage} this.emoteManager.destroy(); this.peerStoreUnsubscribe(); this.biggestAvailableAreaStoreUnsubscribe(); - iframeListener.unregisterAnswerer('getState'); + iframeListener.unregisterAnswerer("getState"); mediaManager.hideGameOverlay(); diff --git a/front/src/Phaser/Game/PlayerInterface.ts b/front/src/Phaser/Game/PlayerInterface.ts index ab881267..5a81c89a 100644 --- a/front/src/Phaser/Game/PlayerInterface.ts +++ b/front/src/Phaser/Game/PlayerInterface.ts @@ -1,9 +1,10 @@ -import type {BodyResourceDescriptionInterface} from "../Entity/PlayerTextures"; +import type { BodyResourceDescriptionInterface } from "../Entity/PlayerTextures"; export interface PlayerInterface { userId: number; name: string; characterLayers: BodyResourceDescriptionInterface[]; - visitCardUrl: string|null; - companion: string|null; + visitCardUrl: string | null; + companion: string | null; + userUuid: string; } diff --git a/front/src/Phaser/Menu/MenuScene.ts b/front/src/Phaser/Menu/MenuScene.ts index d0d6f982..da59cecb 100644 --- a/front/src/Phaser/Menu/MenuScene.ts +++ b/front/src/Phaser/Menu/MenuScene.ts @@ -18,6 +18,7 @@ import { registerMenuCommandStream } from "../../Api/Events/ui/MenuItemRegisterE import { sendMenuClickedEvent } from "../../Api/iframe/Ui/MenuItem"; import { consoleGlobalMessageManagerVisibleStore } from "../../Stores/ConsoleGlobalMessageManagerStore"; import { get } from "svelte/store"; +import { playersStore } from "../../Stores/PlayersStore"; export const MenuSceneName = "MenuScene"; const gameMenuKey = "gameMenu"; @@ -120,7 +121,11 @@ export class MenuScene extends Phaser.Scene { showReportScreenStore.subscribe((user) => { if (user !== null) { this.closeAll(); - this.gameReportElement.open(user.userId, user.userName); + const uuid = playersStore.getPlayerById(user.userId)?.userUuid; + if (uuid === undefined) { + throw new Error("Could not find UUID for user with ID " + user.userId); + } + this.gameReportElement.open(uuid, user.userName); } }); diff --git a/front/src/Phaser/Menu/ReportMenu.ts b/front/src/Phaser/Menu/ReportMenu.ts index e8b20531..effb92b2 100644 --- a/front/src/Phaser/Menu/ReportMenu.ts +++ b/front/src/Phaser/Menu/ReportMenu.ts @@ -1,15 +1,16 @@ -import {MenuScene} from "./MenuScene"; -import {gameManager} from "../Game/GameManager"; -import {blackListManager} from "../../WebRtc/BlackListManager"; +import { MenuScene } from "./MenuScene"; +import { gameManager } from "../Game/GameManager"; +import { blackListManager } from "../../WebRtc/BlackListManager"; +import { playersStore } from "../../Stores/PlayersStore"; -export const gameReportKey = 'gameReport'; -export const gameReportRessource = 'resources/html/gameReport.html'; +export const gameReportKey = "gameReport"; +export const gameReportRessource = "resources/html/gameReport.html"; export class ReportMenu extends Phaser.GameObjects.DOMElement { private opened: boolean = false; - private userId!: number; - private userName!: string|undefined; + private userUuid!: string; + private userName!: string | undefined; private anonymous: boolean; constructor(scene: Phaser.Scene, anonymous: boolean) { @@ -18,46 +19,46 @@ export class ReportMenu extends Phaser.GameObjects.DOMElement { this.createFromCache(gameReportKey); if (this.anonymous) { - const divToHide = this.getChildByID('reportSection') as HTMLElement; + const divToHide = this.getChildByID("reportSection") as HTMLElement; divToHide.hidden = true; - const textToHide = this.getChildByID('askActionP') as HTMLElement; + const textToHide = this.getChildByID("askActionP") as HTMLElement; textToHide.hidden = true; } scene.add.existing(this); MenuScene.revealMenusAfterInit(this, gameReportKey); - this.addListener('click'); - this.on('click', (event:MouseEvent) => { + this.addListener("click"); + this.on("click", (event: MouseEvent) => { event.preventDefault(); - if ((event?.target as HTMLInputElement).id === 'gameReportFormSubmit') { + if ((event?.target as HTMLInputElement).id === "gameReportFormSubmit") { this.submitReport(); - } else if((event?.target as HTMLInputElement).id === 'gameReportFormCancel') { + } else if ((event?.target as HTMLInputElement).id === "gameReportFormCancel") { this.close(); - } else if((event?.target as HTMLInputElement).id === 'toggleBlockButton') { + } else if ((event?.target as HTMLInputElement).id === "toggleBlockButton") { this.toggleBlock(); } }); } - public open(userId: number, userName: string|undefined): void { + public open(userUuid: string, userName: string | undefined): void { if (this.opened) { this.close(); return; } - this.userId = userId; + this.userUuid = userUuid; this.userName = userName; - const mainEl = this.getChildByID('gameReport') as HTMLElement; + const mainEl = this.getChildByID("gameReport") as HTMLElement; this.x = this.getCenteredX(mainEl); this.y = this.getHiddenY(mainEl); - const gameTitleReport = this.getChildByID('nameReported') as HTMLElement; - gameTitleReport.innerText = userName || ''; + const gameTitleReport = this.getChildByID("nameReported") as HTMLElement; + gameTitleReport.innerText = userName || ""; - const blockButton = this.getChildByID('toggleBlockButton') as HTMLElement; - blockButton.innerText = blackListManager.isBlackListed(this.userId) ? 'Unblock this user' : 'Block this user'; + const blockButton = this.getChildByID("toggleBlockButton") as HTMLElement; + blockButton.innerText = blackListManager.isBlackListed(this.userUuid) ? "Unblock this user" : "Block this user"; this.opened = true; @@ -67,19 +68,19 @@ export class ReportMenu extends Phaser.GameObjects.DOMElement { targets: this, y: this.getCenteredY(mainEl), duration: 1000, - ease: 'Power3' + ease: "Power3", }); } public close(): void { gameManager.getCurrentGameScene(this.scene).userInputManager.restoreControls(); this.opened = false; - const mainEl = this.getChildByID('gameReport') as HTMLElement; + const mainEl = this.getChildByID("gameReport") as HTMLElement; this.scene.tweens.add({ targets: this, y: this.getHiddenY(mainEl), duration: 1000, - ease: 'Power3' + ease: "Power3", }); } @@ -88,31 +89,32 @@ export class ReportMenu extends Phaser.GameObjects.DOMElement { return window.innerWidth / 4 - mainEl.clientWidth / 2; } private getHiddenY(mainEl: HTMLElement): number { - return - mainEl.clientHeight - 50; + return -mainEl.clientHeight - 50; } private getCenteredY(mainEl: HTMLElement): number { return window.innerHeight / 4 - mainEl.clientHeight / 2; } private toggleBlock(): void { - !blackListManager.isBlackListed(this.userId) ? blackListManager.blackList(this.userId) : blackListManager.cancelBlackList(this.userId); + !blackListManager.isBlackListed(this.userUuid) + ? blackListManager.blackList(this.userUuid) + : blackListManager.cancelBlackList(this.userUuid); this.close(); } - private submitReport(): void{ - const gamePError = this.getChildByID('gameReportErr') as HTMLParagraphElement; - gamePError.innerText = ''; - gamePError.style.display = 'none'; - const gameTextArea = this.getChildByID('gameReportInput') as HTMLInputElement; - if(!gameTextArea || !gameTextArea.value){ - gamePError.innerText = 'Report message cannot to be empty.'; - gamePError.style.display = 'block'; + private submitReport(): void { + const gamePError = this.getChildByID("gameReportErr") as HTMLParagraphElement; + gamePError.innerText = ""; + gamePError.style.display = "none"; + const gameTextArea = this.getChildByID("gameReportInput") as HTMLInputElement; + if (!gameTextArea || !gameTextArea.value) { + gamePError.innerText = "Report message cannot to be empty."; + gamePError.style.display = "block"; return; } - gameManager.getCurrentGameScene(this.scene).connection?.emitReportPlayerMessage( - this.userId, - gameTextArea.value - ); + gameManager + .getCurrentGameScene(this.scene) + .connection?.emitReportPlayerMessage(this.userUuid, gameTextArea.value); this.close(); } } diff --git a/front/src/Stores/PlayersStore.ts b/front/src/Stores/PlayersStore.ts index ef19efce..6c21de7a 100644 --- a/front/src/Stores/PlayersStore.ts +++ b/front/src/Stores/PlayersStore.ts @@ -1,6 +1,6 @@ import { writable } from "svelte/store"; -import type {PlayerInterface} from "../Phaser/Game/PlayerInterface"; -import type {RoomConnection} from "../Connexion/RoomConnection"; +import type { PlayerInterface } from "../Phaser/Game/PlayerInterface"; +import type { RoomConnection } from "../Connexion/RoomConnection"; /** * A store that contains the list of players currently known. @@ -23,6 +23,7 @@ function createPlayersStore() { characterLayers: message.characterLayers, visitCardUrl: message.visitCardUrl, companion: message.companion, + userUuid: message.userUuid, }); return users; }); @@ -34,9 +35,9 @@ function createPlayersStore() { }); }); }, - getPlayerById(userId: number): PlayerInterface|undefined { + getPlayerById(userId: number): PlayerInterface | undefined { return players.get(userId); - } + }, }; } diff --git a/front/src/WebRtc/BlackListManager.ts b/front/src/WebRtc/BlackListManager.ts index 65efef3a..d2e7c390 100644 --- a/front/src/WebRtc/BlackListManager.ts +++ b/front/src/WebRtc/BlackListManager.ts @@ -1,24 +1,27 @@ -import {Subject} from 'rxjs'; +import { Subject } from "rxjs"; class BlackListManager { - private list: number[] = []; - public onBlockStream: Subject = new Subject(); - public onUnBlockStream: Subject = new Subject(); - - isBlackListed(userId: number): boolean { - return this.list.find((data) => data === userId) !== undefined; - } - - blackList(userId: number): void { - if (this.isBlackListed(userId)) return; - this.list.push(userId); - this.onBlockStream.next(userId); + private list: string[] = []; + public onBlockStream: Subject = new Subject(); + public onUnBlockStream: Subject = new Subject(); + + isBlackListed(userUuid: string): boolean { + return this.list.find((data) => data === userUuid) !== undefined; } - cancelBlackList(userId: number): void { - this.list.splice(this.list.findIndex(data => data === userId), 1); - this.onUnBlockStream.next(userId); + blackList(userUuid: string): void { + if (this.isBlackListed(userUuid)) return; + this.list.push(userUuid); + this.onBlockStream.next(userUuid); + } + + cancelBlackList(userUuid: string): void { + this.list.splice( + this.list.findIndex((data) => data === userUuid), + 1 + ); + this.onUnBlockStream.next(userUuid); } } -export const blackListManager = new BlackListManager(); \ No newline at end of file +export const blackListManager = new BlackListManager(); diff --git a/front/src/WebRtc/SimplePeer.ts b/front/src/WebRtc/SimplePeer.ts index 0d3c4745..5045a5a3 100644 --- a/front/src/WebRtc/SimplePeer.ts +++ b/front/src/WebRtc/SimplePeer.ts @@ -11,7 +11,7 @@ import { get } from "svelte/store"; import { localStreamStore, LocalStreamStoreValue, obtainedMediaConstraintStore } from "../Stores/MediaStore"; import { screenSharingLocalStreamStore } from "../Stores/ScreenSharingStore"; import { discussionManager } from "./DiscussionManager"; -import {playersStore} from "../Stores/PlayersStore"; +import { playersStore } from "../Stores/PlayersStore"; export interface UserSimplePeerInterface { userId: number; @@ -199,7 +199,7 @@ export class SimplePeer { } private getName(userId: number): string { - return playersStore.getPlayerById(userId)?.name || ''; + return playersStore.getPlayerById(userId)?.name || ""; } /** @@ -364,7 +364,8 @@ export class SimplePeer { } private receiveWebrtcScreenSharingSignal(data: WebRtcSignalReceivedMessageInterface) { - if (blackListManager.isBlackListed(data.userId)) return; + const uuid = playersStore.getPlayerById(data.userId)?.userUuid || ""; + if (blackListManager.isBlackListed(uuid)) return; console.log("receiveWebrtcScreenSharingSignal", data); const streamResult = get(screenSharingLocalStreamStore); let stream: MediaStream | null = null; @@ -465,7 +466,8 @@ export class SimplePeer { } private sendLocalScreenSharingStreamToUser(userId: number, localScreenCapture: MediaStream): void { - if (blackListManager.isBlackListed(userId)) return; + const uuid = playersStore.getPlayerById(userId)?.userUuid || ""; + if (blackListManager.isBlackListed(uuid)) return; // If a connection already exists with user (because it is already sharing a screen with us... let's use this connection) if (this.PeerScreenSharingConnectionArray.has(userId)) { this.pushScreenSharingToRemoteUser(userId, localScreenCapture); diff --git a/front/src/WebRtc/VideoPeer.ts b/front/src/WebRtc/VideoPeer.ts index 30328c75..bde0bcde 100644 --- a/front/src/WebRtc/VideoPeer.ts +++ b/front/src/WebRtc/VideoPeer.ts @@ -8,6 +8,7 @@ import type { UserSimplePeerInterface } from "./SimplePeer"; import { get, readable, Readable } from "svelte/store"; import { obtainedMediaConstraintStore } from "../Stores/MediaStore"; import { discussionManager } from "./DiscussionManager"; +import { playersStore } from "../Stores/PlayersStore"; const Peer: SimplePeerNamespace.SimplePeer = require("simple-peer"); @@ -26,6 +27,7 @@ export class VideoPeer extends Peer { private remoteStream!: MediaStream; private blocked: boolean = false; public readonly userId: number; + public readonly userUuid: string; public readonly uniqueId: string; private onBlockSubscribe: Subscription; private onUnBlockSubscribe: Subscription; @@ -60,6 +62,7 @@ export class VideoPeer extends Peer { }); this.userId = user.userId; + this.userUuid = playersStore.getPlayerById(this.userId)?.userUuid || ""; this.uniqueId = "video_" + this.userId; this.streamStore = readable(null, (set) => { @@ -181,20 +184,20 @@ export class VideoPeer extends Peer { }); this.pushVideoToRemoteUser(localStream); - this.onBlockSubscribe = blackListManager.onBlockStream.subscribe((userId) => { - if (userId === this.userId) { + this.onBlockSubscribe = blackListManager.onBlockStream.subscribe((userUuid) => { + if (userUuid === this.userUuid) { this.toggleRemoteStream(false); this.sendBlockMessage(true); } }); - this.onUnBlockSubscribe = blackListManager.onUnBlockStream.subscribe((userId) => { - if (userId === this.userId) { + this.onUnBlockSubscribe = blackListManager.onUnBlockStream.subscribe((userUuid) => { + if (userUuid === this.userUuid) { this.toggleRemoteStream(true); this.sendBlockMessage(false); } }); - if (blackListManager.isBlackListed(this.userId)) { + if (blackListManager.isBlackListed(this.userUuid)) { this.sendBlockMessage(true); } } @@ -231,7 +234,7 @@ export class VideoPeer extends Peer { private stream(stream: MediaStream) { try { this.remoteStream = stream; - if (blackListManager.isBlackListed(this.userId) || this.blocked) { + if (blackListManager.isBlackListed(this.userUuid) || this.blocked) { this.toggleRemoteStream(false); } } catch (err) { diff --git a/messages/protos/messages.proto b/messages/protos/messages.proto index 27d7cb10..a2e55bd8 100644 --- a/messages/protos/messages.proto +++ b/messages/protos/messages.proto @@ -62,7 +62,7 @@ message WebRtcSignalToServerMessage { } message ReportPlayerMessage { - int32 reportedUserId = 1; + string reportedUserUuid = 1; string reportComment = 2; } @@ -158,6 +158,7 @@ message UserJoinedMessage { PositionMessage position = 4; CompanionMessage companion = 5; string visitCardUrl = 6; + string userUuid = 7; } message UserLeftMessage { @@ -285,6 +286,7 @@ message UserJoinedZoneMessage { Zone fromZone = 5; CompanionMessage companion = 6; string visitCardUrl = 7; + string userUuid = 8; } message UserLeftZoneMessage { diff --git a/pusher/src/Model/Zone.ts b/pusher/src/Model/Zone.ts index 8eeeb3ef..501a2541 100644 --- a/pusher/src/Model/Zone.ts +++ b/pusher/src/Model/Zone.ts @@ -39,6 +39,7 @@ export type LeavesCallback = (thing: Movable, listener: User) => void;*/ export class UserDescriptor { private constructor( public readonly userId: number, + private userUuid: string, private name: string, private characterLayers: CharacterLayerMessage[], private position: PositionMessage, @@ -57,6 +58,7 @@ export class UserDescriptor { } return new UserDescriptor( message.getUserid(), + message.getUseruuid(), message.getName(), message.getCharacterlayersList(), position, @@ -84,6 +86,7 @@ export class UserDescriptor { userJoinedMessage.setVisitcardurl(this.visitCardUrl); } userJoinedMessage.setCompanion(this.companion); + userJoinedMessage.setUseruuid(this.userUuid); return userJoinedMessage; } diff --git a/pusher/src/Services/SocketManager.ts b/pusher/src/Services/SocketManager.ts index 8a0d3673..cfac5946 100644 --- a/pusher/src/Services/SocketManager.ts +++ b/pusher/src/Services/SocketManager.ts @@ -61,7 +61,6 @@ export interface AdminSocketData { export class SocketManager implements ZoneEventListener { private rooms: Map = new Map(); - private sockets: Map = new Map(); constructor() { clientEventsEmitter.registerToClientJoin((clientUUid: string, roomId: string) => { @@ -191,8 +190,6 @@ export class SocketManager implements ZoneEventListener { .on("data", (message: ServerToClientMessage) => { if (message.hasRoomjoinedmessage()) { client.userId = (message.getRoomjoinedmessage() as RoomJoinedMessage).getCurrentuserid(); - // TODO: do we need this.sockets anymore? - this.sockets.set(client.userId, client); // If this is the first message sent, send back the viewport. this.handleViewport(client, viewport); @@ -302,14 +299,8 @@ export class SocketManager implements ZoneEventListener { async handleReportMessage(client: ExSocketInterface, reportPlayerMessage: ReportPlayerMessage) { try { - const reportedSocket = this.sockets.get(reportPlayerMessage.getReporteduserid()); - if (!reportedSocket) { - throw "reported socket user not found"; - } - //TODO report user on admin application - //todo: move to back because this fail if the reported player is in another pusher. await adminApi.reportPlayer( - reportedSocket.userUuid, + reportPlayerMessage.getReporteduseruuid(), reportPlayerMessage.getReportcomment(), client.userUuid, client.roomId.split("/")[2] @@ -334,14 +325,6 @@ export class SocketManager implements ZoneEventListener { socket.backConnection.write(pusherToBackMessage); } - private searchClientByIdOrFail(userId: number): ExSocketInterface { - const client: ExSocketInterface | undefined = this.sockets.get(userId); - if (client === undefined) { - throw new Error("Could not find user with id " + userId); - } - return client; - } - leaveRoom(socket: ExSocketInterface) { // leave previous room and world try { @@ -364,9 +347,8 @@ export class SocketManager implements ZoneEventListener { //Client.leave(Client.roomId); } finally { //delete Client.roomId; - this.sockets.delete(socket.userId); clientEventsEmitter.emitClientLeave(socket.userUuid, socket.roomId); - console.log("A user left (", this.sockets.size, " connected users)"); + console.log("A user left"); } } } finally { @@ -410,15 +392,6 @@ export class SocketManager implements ZoneEventListener { return this.rooms; } - searchClientByUuid(uuid: string): ExSocketInterface | null { - for (const socket of this.sockets.values()) { - if (socket.userUuid === uuid) { - return socket; - } - } - return null; - } - public handleQueryJitsiJwtMessage(client: ExSocketInterface, queryJitsiJwtMessage: QueryJitsiJwtMessage) { try { const room = queryJitsiJwtMessage.getJitsiroom(); From d51ac45079ef40f5304a63b8186596692c3d6feb Mon Sep 17 00:00:00 2001 From: GRL Date: Wed, 7 Jul 2021 14:26:53 +0200 Subject: [PATCH 014/101] Show/Hide Layer now unset collision and can show/hide all the layer in a group layer --- front/src/Api/Events/LayerEvent.ts | 1 + front/src/Api/iframe/room.ts | 10 ++++---- front/src/Phaser/Game/GameMap.ts | 4 +++ front/src/Phaser/Game/GameScene.ts | 39 +++++++++++++++++++++--------- 4 files changed, 38 insertions(+), 16 deletions(-) diff --git a/front/src/Api/Events/LayerEvent.ts b/front/src/Api/Events/LayerEvent.ts index b56c3163..d3fdda22 100644 --- a/front/src/Api/Events/LayerEvent.ts +++ b/front/src/Api/Events/LayerEvent.ts @@ -3,6 +3,7 @@ import * as tg from "generic-type-guard"; export const isLayerEvent = new tg.IsInterface() .withProperties({ name: tg.isString, + group: tg.isBoolean, }) .get(); /** diff --git a/front/src/Api/iframe/room.ts b/front/src/Api/iframe/room.ts index c70d0aad..8ff31375 100644 --- a/front/src/Api/iframe/room.ts +++ b/front/src/Api/iframe/room.ts @@ -4,7 +4,7 @@ import { isDataLayerEvent } from "../Events/DataLayerEvent"; import { EnterLeaveEvent, isEnterLeaveEvent } from "../Events/EnterLeaveEvent"; import { isGameStateEvent } from "../Events/GameStateEvent"; -import {IframeApiContribution, queryWorkadventure, sendToWorkadventure} from "./IframeApiContribution"; +import { IframeApiContribution, queryWorkadventure, sendToWorkadventure } from "./IframeApiContribution"; import { apiCallback } from "./registeredCallbacks"; import type { ITiledMap } from "../../Phaser/Map/ITiledMap"; @@ -93,11 +93,11 @@ export class WorkadventureRoomCommands extends IframeApiContribution layer.layer.name === layerName); } + public findPhaserLayers(groupName: string): TilemapLayer[] { + return this.phaserLayers.filter((l) => l.layer.name.includes(groupName)); + } + public addTerrain(terrain: Phaser.Tilemaps.Tileset): void { for (const phaserLayer of this.phaserLayers) { phaserLayer.tileset.push(terrain); diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index d767f0f4..5c3edfac 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -1022,13 +1022,13 @@ ${escapedMessage} this.iframeSubscriptionList.push( iframeListener.showLayerStream.subscribe((layerEvent) => { - this.setLayerVisibility(layerEvent.name, true); + this.setLayerVisibility(layerEvent.name, true, layerEvent.group); }) ); this.iframeSubscriptionList.push( iframeListener.hideLayerStream.subscribe((layerEvent) => { - this.setLayerVisibility(layerEvent.name, false); + this.setLayerVisibility(layerEvent.name, false, layerEvent.group); }) ); @@ -1044,7 +1044,7 @@ ${escapedMessage} }) ); - iframeListener.registerAnswerer('getState', () => { + iframeListener.registerAnswerer("getState", () => { return { mapUrl: this.MapUrlFile, startLayerName: this.startPositionCalculator.startLayerName, @@ -1084,14 +1084,31 @@ ${escapedMessage} property.value = propertyValue; } - private setLayerVisibility(layerName: string, visible: boolean): void { - const phaserLayer = this.gameMap.findPhaserLayer(layerName); - if (phaserLayer === undefined) { - console.warn('Could not find layer "' + layerName + '" when calling WA.hideLayer / WA.showLayer'); - return; + private setLayerVisibility(layerName: string, visible: boolean, group: boolean): void { + if (group) { + const phaserLayers = this.gameMap.findPhaserLayers(layerName); + if (phaserLayers === []) { + console.warn( + 'Could not find layer with name that contains "' + + layerName + + '" when calling WA.hideLayer / WA.showLayer' + ); + return; + } + for (let i = 0; i < phaserLayers.length; i++) { + phaserLayers[i].setVisible(visible); + phaserLayers[i].setCollisionByProperty({ collides: true }, visible); + } + } else { + const phaserLayer = this.gameMap.findPhaserLayer(layerName); + if (phaserLayer === undefined) { + console.warn('Could not find layer "' + layerName + '" when calling WA.hideLayer / WA.showLayer'); + return; + } + phaserLayer.setVisible(visible); + phaserLayer.setCollisionByProperty({ collides: true }, visible); } - phaserLayer.setVisible(visible); - this.dirty = true; + this.markDirty(); } private getMapDirUrl(): string { @@ -1147,7 +1164,7 @@ ${escapedMessage} this.emoteManager.destroy(); this.peerStoreUnsubscribe(); this.biggestAvailableAreaStoreUnsubscribe(); - iframeListener.unregisterAnswerer('getState'); + iframeListener.unregisterAnswerer("getState"); mediaManager.hideGameOverlay(); From bef5e139c0d5fbcada2925bc5a3a54836986fdf7 Mon Sep 17 00:00:00 2001 From: GRL Date: Wed, 7 Jul 2021 14:42:17 +0200 Subject: [PATCH 015/101] SetTiles can now set a tile to null so that there is no more tile. --- front/src/Api/Events/SetTilesEvent.ts | 2 +- front/src/Api/iframe/room.ts | 4 ++-- front/src/Phaser/Game/GameMap.ts | 33 +++++++++++++++++++-------- 3 files changed, 27 insertions(+), 12 deletions(-) diff --git a/front/src/Api/Events/SetTilesEvent.ts b/front/src/Api/Events/SetTilesEvent.ts index c7f8f16d..371f0884 100644 --- a/front/src/Api/Events/SetTilesEvent.ts +++ b/front/src/Api/Events/SetTilesEvent.ts @@ -5,7 +5,7 @@ export const isSetTilesEvent = tg.isArray( .withProperties({ x: tg.isNumber, y: tg.isNumber, - tile: tg.isUnion(tg.isNumber, tg.isString), + tile: tg.isUnion(tg.isUnion(tg.isNumber, tg.isString), tg.isNull), layer: tg.isString, }) .get() diff --git a/front/src/Api/iframe/room.ts b/front/src/Api/iframe/room.ts index c70d0aad..a9ee52ce 100644 --- a/front/src/Api/iframe/room.ts +++ b/front/src/Api/iframe/room.ts @@ -4,7 +4,7 @@ import { isDataLayerEvent } from "../Events/DataLayerEvent"; import { EnterLeaveEvent, isEnterLeaveEvent } from "../Events/EnterLeaveEvent"; import { isGameStateEvent } from "../Events/GameStateEvent"; -import {IframeApiContribution, queryWorkadventure, sendToWorkadventure} from "./IframeApiContribution"; +import { IframeApiContribution, queryWorkadventure, sendToWorkadventure } from "./IframeApiContribution"; import { apiCallback } from "./registeredCallbacks"; import type { ITiledMap } from "../../Phaser/Map/ITiledMap"; @@ -34,7 +34,7 @@ interface User { interface TileDescriptor { x: number; y: number; - tile: number | string; + tile: number | string | null; layer: string; } diff --git a/front/src/Phaser/Game/GameMap.ts b/front/src/Phaser/Game/GameMap.ts index a616cf4a..1f232265 100644 --- a/front/src/Phaser/Game/GameMap.ts +++ b/front/src/Phaser/Game/GameMap.ts @@ -152,7 +152,10 @@ export class GameMap { } private getTileProperty(index: number): Array { - return this.tileSetPropertyMap[index]; + if (this.tileSetPropertyMap[index]) { + return this.tileSetPropertyMap[index]; + } + return []; } private trigger( @@ -198,37 +201,49 @@ export class GameMap { private putTileInFlatLayer(index: number, x: number, y: number, layer: string): void { const fLayer = this.findLayer(layer); if (fLayer == undefined) { - console.error("The layer that you want to change doesn't exist."); + console.error("The layer '" + layer + "' that you want to change doesn't exist."); return; } if (fLayer.type !== "tilelayer") { - console.error("The layer that you want to change is not a tilelayer. Tile can only be put in tilelayer."); + console.error( + "The layer '" + + layer + + "' that you want to change is not a tilelayer. Tile can only be put in tilelayer." + ); return; } if (typeof fLayer.data === "string") { - console.error("Data of the layer that you want to change is only readable."); + console.error("Data of the layer '" + layer + "' that you want to change is only readable."); return; } - fLayer.data[x + y * fLayer.height] = index; + fLayer.data[x + y * fLayer.width] = index; } - public putTile(tile: string | number, x: number, y: number, layer: string): void { + public putTile(tile: string | number | null, x: number, y: number, layer: string): void { const phaserLayer = this.findPhaserLayer(layer); if (phaserLayer) { + if (tile === null) { + phaserLayer.putTileAt(-1, x, y); + return; + } const tileIndex = this.getIndexForTileType(tile); if (tileIndex !== undefined) { this.putTileInFlatLayer(tileIndex, x, y, layer); const phaserTile = phaserLayer.putTileAt(tileIndex, x, y); for (const property of this.getTileProperty(tileIndex)) { - if (property.name === "collides" && property.value === "true") { + if (property.name === "collides" && property.value) { phaserTile.setCollision(true); } } } else { - console.error("The tile that you want to place doesn't exist."); + console.error("The tile '" + tile + "' that you want to place doesn't exist."); } } else { - console.error("The layer that you want to change is not a tilelayer. Tile can only be put in tilelayer."); + console.error( + "The layer '" + + layer + + "' that you want to change is not a tilelayer. Tile can only be put in tilelayer." + ); } } From 24811e0a31a70a4d32fe3fda16d0f71274d6f872 Mon Sep 17 00:00:00 2001 From: GRL Date: Wed, 7 Jul 2021 14:59:40 +0200 Subject: [PATCH 016/101] SetProperty delete a property where tha value is undefined and load the map of exitUrl property --- front/src/Phaser/Game/GameScene.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index d6df242f..6427e0c0 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -1077,14 +1077,24 @@ ${escapedMessage} console.warn('Could not find layer "' + layerName + '" when calling setProperty'); return; } + if (propertyName === "exitUrl" && typeof propertyValue === "string") { + this.loadNextGame(propertyValue); + } if (layer.properties === undefined) { layer.properties = []; } const property = layer.properties.find((property) => property.name === propertyName); if (property === undefined) { + if (propertyValue === undefined) { + return; + } layer.properties.push({ name: propertyName, type: typeof propertyValue, value: propertyValue }); return; } + if (propertyValue === undefined) { + const index = layer.properties.indexOf(property); + layer.properties.splice(index, 1); + } property.value = propertyValue; } From 17525e1e158256023efda437e88a1f04f4d2bef6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?gr=C3=A9goire=20parant?= Date: Wed, 7 Jul 2021 16:42:26 +0200 Subject: [PATCH 017/101] Return at the new line into the Pop-up (#1267) Add regex to replace "\r\n" or "\r" or "\n" by
--- front/src/WebRtc/HtmlUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/front/src/WebRtc/HtmlUtils.ts b/front/src/WebRtc/HtmlUtils.ts index 942e553f..569abd07 100644 --- a/front/src/WebRtc/HtmlUtils.ts +++ b/front/src/WebRtc/HtmlUtils.ts @@ -25,7 +25,7 @@ export class HtmlUtils { } public static escapeHtml(html: string): string { - const text = document.createTextNode(html); + const text = document.createTextNode(html.replace(/(\r\n|\r|\n)/g,'
')); const p = document.createElement('p'); p.appendChild(text); return p.innerHTML; From e50292a2ba42814fd6759df68abef10b26a3723b Mon Sep 17 00:00:00 2001 From: GRL Date: Wed, 7 Jul 2021 16:58:54 +0200 Subject: [PATCH 018/101] Add documentation No second parameter --- docs/maps/api-room.md | 4 ++++ front/src/Api/Events/LayerEvent.ts | 1 - front/src/Api/iframe/room.ts | 8 ++++---- front/src/Phaser/Game/GameScene.ts | 22 +++++++++------------- 4 files changed, 17 insertions(+), 18 deletions(-) diff --git a/docs/maps/api-room.md b/docs/maps/api-room.md index 9d08ce1b..86886567 100644 --- a/docs/maps/api-room.md +++ b/docs/maps/api-room.md @@ -54,6 +54,7 @@ WA.room.showLayer(layerName : string): void WA.room.hideLayer(layerName : string) : void ``` These 2 methods can be used to show and hide a layer. +if `layerName` is the name of a group layer, show/hide all the layer in that group layer. Example : ```javascript @@ -70,6 +71,9 @@ WA.room.setProperty(layerName : string, propertyName : string, propertyValue : s Set the value of the `propertyName` property of the layer `layerName` at `propertyValue`. If the property doesn't exist, create the property `propertyName` and set the value of the property at `propertyValue`. +Note : +To unset a property form a layer, use `setProperty` with `propertyValue` set to `undefined`. + Example : ```javascript WA.room.setProperty('wikiLayer', 'openWebsite', 'https://www.wikipedia.org/'); diff --git a/front/src/Api/Events/LayerEvent.ts b/front/src/Api/Events/LayerEvent.ts index d3fdda22..b56c3163 100644 --- a/front/src/Api/Events/LayerEvent.ts +++ b/front/src/Api/Events/LayerEvent.ts @@ -3,7 +3,6 @@ import * as tg from "generic-type-guard"; export const isLayerEvent = new tg.IsInterface() .withProperties({ name: tg.isString, - group: tg.isBoolean, }) .get(); /** diff --git a/front/src/Api/iframe/room.ts b/front/src/Api/iframe/room.ts index 8ff31375..deee0e2a 100644 --- a/front/src/Api/iframe/room.ts +++ b/front/src/Api/iframe/room.ts @@ -93,11 +93,11 @@ export class WorkadventureRoomCommands extends IframeApiContribution { - this.setLayerVisibility(layerEvent.name, true, layerEvent.group); + this.setLayerVisibility(layerEvent.name, true); }) ); this.iframeSubscriptionList.push( iframeListener.hideLayerStream.subscribe((layerEvent) => { - this.setLayerVisibility(layerEvent.name, false, layerEvent.group); + this.setLayerVisibility(layerEvent.name, false); }) ); @@ -1088,9 +1088,13 @@ ${escapedMessage} property.value = propertyValue; } - private setLayerVisibility(layerName: string, visible: boolean, group: boolean): void { - if (group) { - const phaserLayers = this.gameMap.findPhaserLayers(layerName); + private setLayerVisibility(layerName: string, visible: boolean): void { + const phaserLayer = this.gameMap.findPhaserLayer(layerName); + if (phaserLayer != undefined) { + phaserLayer.setVisible(visible); + phaserLayer.setCollisionByProperty({ collides: true }, visible); + } else { + const phaserLayers = this.gameMap.findPhaserLayers(layerName + "/"); if (phaserLayers === []) { console.warn( 'Could not find layer with name that contains "' + @@ -1103,14 +1107,6 @@ ${escapedMessage} phaserLayers[i].setVisible(visible); phaserLayers[i].setCollisionByProperty({ collides: true }, visible); } - } else { - const phaserLayer = this.gameMap.findPhaserLayer(layerName); - if (phaserLayer === undefined) { - console.warn('Could not find layer "' + layerName + '" when calling WA.hideLayer / WA.showLayer'); - return; - } - phaserLayer.setVisible(visible); - phaserLayer.setCollisionByProperty({ collides: true }, visible); } this.markDirty(); } From 64c569c42f6bf0b1edd1ee039551c7aff0132bb8 Mon Sep 17 00:00:00 2001 From: GRL Date: Wed, 7 Jul 2021 17:06:23 +0200 Subject: [PATCH 019/101] Add documentation and CHANGELOG Modify error message --- CHANGELOG.md | 4 ++-- docs/maps/api-room.md | 1 + front/src/Phaser/Game/GameMap.ts | 6 +----- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a83e8213..ff7496ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,12 +10,12 @@ - New scripting API features : - Use `WA.room.showLayer(): void` to show a layer - Use `WA.room.hideLayer(): void` to hide a layer - - Use `WA.room.setProperty() : void` to add or change existing property of a layer + - Use `WA.room.setProperty() : void` to add, delete or change existing property of a layer - Use `WA.player.onPlayerMove(): void` to track the movement of the current player - Use `WA.room.getCurrentUser(): Promise` to get the ID, name and tags of the current player - Use `WA.room.getCurrentRoom(): Promise` to get the ID, JSON map file, url of the map of the current room and the layer where the current player started - Use `WA.ui.registerMenuCommand(): void` to add a custom menu - - Use `WA.room.setTiles(): void` to change an array of tiles + - Use `WA.room.setTiles(): void` to add, delete or change an array of tiles - Users blocking now relies on UUID rather than ID. A blocked user that leaves a room and comes back will stay blocked. ## Version 1.4.3 - 1.4.4 - 1.4.5 diff --git a/docs/maps/api-room.md b/docs/maps/api-room.md index 9d08ce1b..22735f6c 100644 --- a/docs/maps/api-room.md +++ b/docs/maps/api-room.md @@ -134,6 +134,7 @@ If `tile` is a string, it's not the id of the tile but the value of the property **Important !** : If you use `tile` as a number, be sure to add the `firstgid` of the tileset of the tile that you want to the id of the tile in Tiled Editor. +Note: If you want to unset a tile, use `setTiles` with `tile` set to `null`. Example : ```javascript diff --git a/front/src/Phaser/Game/GameMap.ts b/front/src/Phaser/Game/GameMap.ts index 1f232265..99a1edad 100644 --- a/front/src/Phaser/Game/GameMap.ts +++ b/front/src/Phaser/Game/GameMap.ts @@ -239,11 +239,7 @@ export class GameMap { console.error("The tile '" + tile + "' that you want to place doesn't exist."); } } else { - console.error( - "The layer '" + - layer + - "' that you want to change is not a tilelayer. Tile can only be put in tilelayer." - ); + console.error("The layer '" + layer + "' does not exist (or is not a tilelaye)."); } } From cb5bdb5fea0d800bfecc235e78995d9650facf0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Wed, 7 Jul 2021 17:15:22 +0200 Subject: [PATCH 020/101] Fixing typo --- docs/maps/api-room.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/maps/api-room.md b/docs/maps/api-room.md index 86886567..93cb732a 100644 --- a/docs/maps/api-room.md +++ b/docs/maps/api-room.md @@ -72,7 +72,7 @@ WA.room.setProperty(layerName : string, propertyName : string, propertyValue : s Set the value of the `propertyName` property of the layer `layerName` at `propertyValue`. If the property doesn't exist, create the property `propertyName` and set the value of the property at `propertyValue`. Note : -To unset a property form a layer, use `setProperty` with `propertyValue` set to `undefined`. +To unset a property from a layer, use `setProperty` with `propertyValue` set to `undefined`. Example : ```javascript From e65e8b2097d443ac3be8f504c4ca208ed84a0c34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Wed, 7 Jul 2021 17:17:28 +0200 Subject: [PATCH 021/101] First version with variables that actually work --- back/src/Model/GameRoom.ts | 37 +++++++- back/src/RoomManager.ts | 30 +++++- back/src/Services/SocketManager.ts | 91 ++++++++++++++----- front/src/Api/IframeListener.ts | 16 ++-- front/src/Connexion/ConnexionModels.ts | 2 + front/src/Connexion/RoomConnection.ts | 22 +++++ front/src/Phaser/Game/GameScene.ts | 2 +- .../src/Phaser/Game/SharedVariablesManager.ts | 22 ++++- maps/tests/Variables/shared_variables.html | 41 +++++++++ maps/tests/index.html | 10 +- pusher/src/Model/PusherRoom.ts | 27 ++++-- 11 files changed, 250 insertions(+), 50 deletions(-) create mode 100644 maps/tests/Variables/shared_variables.html diff --git a/back/src/Model/GameRoom.ts b/back/src/Model/GameRoom.ts index 33af483f..ffe3563f 100644 --- a/back/src/Model/GameRoom.ts +++ b/back/src/Model/GameRoom.ts @@ -7,9 +7,14 @@ import { PositionNotifier } from "./PositionNotifier"; import { Movable } from "_Model/Movable"; import { extractDataFromPrivateRoomId, extractRoomSlugPublicRoomId, isRoomAnonymous } from "./RoomIdentifier"; import { arrayIntersect } from "../Services/ArrayHelper"; -import { EmoteEventMessage, JoinRoomMessage } from "../Messages/generated/messages_pb"; +import { + BatchToPusherMessage, + BatchToPusherRoomMessage, + EmoteEventMessage, + JoinRoomMessage, SubToPusherRoomMessage, VariableMessage +} from "../Messages/generated/messages_pb"; import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils"; -import { ZoneSocket } from "src/RoomManager"; +import {RoomSocket, ZoneSocket} from "src/RoomManager"; import { Admin } from "../Model/Admin"; export type ConnectCallback = (user: User, group: Group) => void; @@ -35,7 +40,7 @@ export class GameRoom { private readonly disconnectCallback: DisconnectCallback; private itemsState = new Map(); - private variables = new Map(); + public readonly variables = new Map(); private readonly positionNotifier: PositionNotifier; public readonly roomId: string; @@ -45,6 +50,8 @@ export class GameRoom { private versionNumber: number = 1; private nextUserId: number = 1; + private roomListeners: Set = new Set(); + constructor( roomId: string, connectCallback: ConnectCallback, @@ -312,6 +319,22 @@ export class GameRoom { public setVariable(name: string, value: string): void { this.variables.set(name, value); + + // TODO: should we batch those every 100ms? + const variableMessage = new VariableMessage(); + variableMessage.setName(name); + variableMessage.setValue(value); + + const subMessage = new SubToPusherRoomMessage(); + subMessage.setVariablemessage(variableMessage); + + const batchMessage = new BatchToPusherRoomMessage(); + batchMessage.addPayload(subMessage); + + // Dispatch the message on the room listeners + for (const socket of this.roomListeners) { + socket.write(batchMessage); + } } public addZoneListener(call: ZoneSocket, x: number, y: number): Set { @@ -343,4 +366,12 @@ export class GameRoom { public emitEmoteEvent(user: User, emoteEventMessage: EmoteEventMessage) { this.positionNotifier.emitEmoteEvent(user, emoteEventMessage); } + + public addRoomListener(socket: RoomSocket) { + this.roomListeners.add(socket); + } + + public removeRoomListener(socket: RoomSocket) { + this.roomListeners.delete(socket); + } } diff --git a/back/src/RoomManager.ts b/back/src/RoomManager.ts index 2514c576..d4dcc6d4 100644 --- a/back/src/RoomManager.ts +++ b/back/src/RoomManager.ts @@ -4,7 +4,7 @@ import { AdminMessage, AdminPusherToBackMessage, AdminRoomMessage, - BanMessage, + BanMessage, BatchToPusherRoomMessage, EmotePromptMessage, EmptyMessage, ItemEventMessage, @@ -12,7 +12,7 @@ import { PlayGlobalMessage, PusherToBackMessage, QueryJitsiJwtMessage, - RefreshRoomPromptMessage, + RefreshRoomPromptMessage, RoomMessage, ServerToAdminClientMessage, ServerToClientMessage, SilentMessage, @@ -33,6 +33,7 @@ const debug = Debug("roommanager"); export type AdminSocket = ServerDuplexStream; export type ZoneSocket = ServerWritableStream; +export type RoomSocket = ServerWritableStream; const roomManager: IRoomManagerServer = { joinRoom: (call: UserSocket): void => { @@ -156,6 +157,29 @@ const roomManager: IRoomManagerServer = { }); }, + listenRoom(call: RoomSocket): void { + debug("listenRoom called"); + const roomMessage = call.request; + + socketManager.addRoomListener(call, roomMessage.getRoomid()); + + call.on("cancelled", () => { + debug("listenRoom cancelled"); + socketManager.removeRoomListener(call, roomMessage.getRoomid()); + call.end(); + }); + + call.on("close", () => { + debug("listenRoom connection closed"); + socketManager.removeRoomListener(call, roomMessage.getRoomid()); + }).on("error", (e) => { + console.error("An error occurred in listenRoom stream:", e); + socketManager.removeRoomListener(call, roomMessage.getRoomid()); + call.end(); + }); + + }, + adminRoom(call: AdminSocket): void { console.log("adminRoom called"); @@ -230,7 +254,7 @@ const roomManager: IRoomManagerServer = { ): void { socketManager.dispatchRoomRefresh(call.request.getRoomid()); callback(null, new EmptyMessage()); - }, + } }; export { roomManager }; diff --git a/back/src/Services/SocketManager.ts b/back/src/Services/SocketManager.ts index 9f655da3..824c8bfb 100644 --- a/back/src/Services/SocketManager.ts +++ b/back/src/Services/SocketManager.ts @@ -30,7 +30,7 @@ import { BanUserMessage, RefreshRoomMessage, EmotePromptMessage, - VariableMessage, + VariableMessage, BatchToPusherRoomMessage, SubToPusherRoomMessage, } from "../Messages/generated/messages_pb"; import { User, UserSocket } from "../Model/User"; import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils"; @@ -49,7 +49,7 @@ import Jwt from "jsonwebtoken"; import { JITSI_URL } from "../Enum/EnvironmentVariable"; import { clientEventsEmitter } from "./ClientEventsEmitter"; import { gaugeManager } from "./GaugeManager"; -import { ZoneSocket } from "../RoomManager"; +import {RoomSocket, ZoneSocket} from "../RoomManager"; import { Zone } from "_Model/Zone"; import Debug from "debug"; import { Admin } from "_Model/Admin"; @@ -66,7 +66,9 @@ function emitZoneMessage(subMessage: SubToPusherMessage, socket: ZoneSocket): vo } export class SocketManager { - private rooms: Map = new Map(); + private rooms = new Map(); + // List of rooms in process of loading. + private roomsPromises = new Map>(); constructor() { clientEventsEmitter.registerToClientJoin((clientUUid: string, roomId: string) => { @@ -102,6 +104,14 @@ export class SocketManager { roomJoinedMessage.addItem(itemStateMessage); } + for (const [name, value] of room.variables.entries()) { + const variableMessage = new VariableMessage(); + variableMessage.setName(name); + variableMessage.setValue(value); + + roomJoinedMessage.addVariable(variableMessage); + } + roomJoinedMessage.setCurrentuserid(user.id); const serverToClientMessage = new ServerToClientMessage(); @@ -186,23 +196,10 @@ export class SocketManager { } handleVariableEvent(room: GameRoom, user: User, variableMessage: VariableMessage) { - const itemEvent = ProtobufUtils.toItemEvent(itemEventMessage); - try { - // TODO: DISPATCH ON NEW ROOM CHANNEL - - const subMessage = new SubMessage(); - subMessage.setItemeventmessage(itemEventMessage); - - // Let's send the event without using the SocketIO room. - // TODO: move this in the GameRoom class. - for (const user of room.getUsers().values()) { - user.emitInBatch(subMessage); - } - room.setVariable(variableMessage.getName(), variableMessage.getValue()); } catch (e) { - console.error('An error occurred on "item_event"'); + console.error('An error occurred on "handleVariableEvent"'); console.error(e); } } @@ -284,10 +281,18 @@ export class SocketManager { } async getOrCreateRoom(roomId: string): Promise { - //check and create new world for a room - let world = this.rooms.get(roomId); - if (world === undefined) { - world = new GameRoom( + //check and create new room + let room = this.rooms.get(roomId); + if (room === undefined) { + let roomPromise = this.roomsPromises.get(roomId); + if (roomPromise) { + return roomPromise; + } + + // Note: for now, the promise is useless (because this is synchronous, but soon, we will need to + // load the map server side. + + room = new GameRoom( roomId, (user: User, group: Group) => this.joinWebRtcRoom(user, group), (user: User, group: Group) => this.disConnectedUser(user, group), @@ -303,9 +308,12 @@ export class SocketManager { this.onEmote(emoteEventMessage, listener) ); gaugeManager.incNbRoomGauge(); - this.rooms.set(roomId, world); + this.rooms.set(roomId, room); + + // TODO: change this the to new Promise()... when the method becomes actually asynchronous + roomPromise = Promise.resolve(room); } - return Promise.resolve(world); + return Promise.resolve(room); } private async joinRoom( @@ -676,6 +684,42 @@ export class SocketManager { room.removeZoneListener(call, x, y); } + async addRoomListener(call: RoomSocket, roomId: string) { + const room = await this.getOrCreateRoom(roomId); + if (!room) { + console.error("In addRoomListener, could not find room with id '" + roomId + "'"); + return; + } + + room.addRoomListener(call); + //const things = room.addZoneListener(call, x, y); + + const batchMessage = new BatchToPusherRoomMessage(); + + for (const [name, value] of room.variables.entries()) { + const variableMessage = new VariableMessage(); + variableMessage.setName(name); + variableMessage.setValue(value); + + const subMessage = new SubToPusherRoomMessage(); + subMessage.setVariablemessage(variableMessage); + + batchMessage.addPayload(subMessage); + } + + call.write(batchMessage); + } + + removeRoomListener(call: RoomSocket, roomId: string) { + const room = this.rooms.get(roomId); + if (!room) { + console.error("In removeRoomListener, could not find room with id '" + roomId + "'"); + return; + } + + room.removeRoomListener(call); + } + public async handleJoinAdminRoom(admin: Admin, roomId: string): Promise { const room = await socketManager.getOrCreateRoom(roomId); @@ -831,6 +875,7 @@ export class SocketManager { emoteEventMessage.setActoruserid(user.id); room.emitEmoteEvent(user, emoteEventMessage); } + } export const socketManager = new SocketManager(); diff --git a/front/src/Api/IframeListener.ts b/front/src/Api/IframeListener.ts index 9c61bdf8..d8559aa0 100644 --- a/front/src/Api/IframeListener.ts +++ b/front/src/Api/IframeListener.ts @@ -34,6 +34,8 @@ import { handleMenuItemRegistrationEvent, isMenuItemRegisterIframeEvent } from " import { SetTilesEvent, isSetTilesEvent } from "./Events/SetTilesEvent"; import { isSetVariableIframeEvent, SetVariableEvent } from "./Events/SetVariableEvent"; +type AnswererCallback = (query: IframeQueryMap[T]['query']) => IframeQueryMap[T]['answer']|PromiseLike; + /** * Listens to messages from iframes and turn those messages into easy to use observables. * Also allows to send messages to those iframes. @@ -111,12 +113,10 @@ class IframeListener { private sendPlayerMove: boolean = false; - // Note: we are forced to type this in "any" because of https://github.com/microsoft/TypeScript/issues/31904 - // eslint-disable-next-line @typescript-eslint/no-explicit-any - private answerers: any = {}; - /*private answerers: { - [key in keyof IframeQueryMap]?: (query: IframeQueryMap[key]['query']) => IframeQueryMap[key]['answer']|PromiseLike - } = {};*/ + // Note: we are forced to type this in unknown and later cast with "as" because of https://github.com/microsoft/TypeScript/issues/31904 + private answerers: { + [str in keyof IframeQueryMap]?: unknown + } = {}; init() { @@ -156,7 +156,7 @@ class IframeListener { const queryId = payload.id; const query = payload.query; - const answerer = this.answerers[query.type]; + const answerer = this.answerers[query.type] as AnswererCallback | undefined; if (answerer === undefined) { const errorMsg = 'The iFrame sent a message of type "'+query.type+'" but there is no service configured to answer these messages.'; console.error(errorMsg); @@ -432,7 +432,7 @@ class IframeListener { * @param key The "type" of the query we are answering * @param callback */ - public registerAnswerer(key: T, callback: (query: IframeQueryMap[T]['query']) => IframeQueryMap[T]['answer']|PromiseLike ): void { + public registerAnswerer(key: T, callback: AnswererCallback ): void { this.answerers[key] = callback; } diff --git a/front/src/Connexion/ConnexionModels.ts b/front/src/Connexion/ConnexionModels.ts index 189aea7c..2f4c414b 100644 --- a/front/src/Connexion/ConnexionModels.ts +++ b/front/src/Connexion/ConnexionModels.ts @@ -31,6 +31,7 @@ export enum EventMessage { TELEPORT = "teleport", USER_MESSAGE = "user-message", START_JITSI_ROOM = "start-jitsi-room", + SET_VARIABLE = "set-variable", } export interface PointInterface { @@ -105,6 +106,7 @@ export interface RoomJoinedMessageInterface { //users: MessageUserPositionInterface[], //groups: GroupCreatedUpdatedMessageInterface[], items: { [itemId: number]: unknown }; + variables: Map; } export interface PlayGlobalMessageInterface { diff --git a/front/src/Connexion/RoomConnection.ts b/front/src/Connexion/RoomConnection.ts index b2836a03..53eff010 100644 --- a/front/src/Connexion/RoomConnection.ts +++ b/front/src/Connexion/RoomConnection.ts @@ -165,6 +165,9 @@ export class RoomConnection implements RoomConnection { } else if (subMessage.hasEmoteeventmessage()) { const emoteMessage = subMessage.getEmoteeventmessage() as EmoteEventMessage; emoteEventStream.fire(emoteMessage.getActoruserid(), emoteMessage.getEmote()); + } else if (subMessage.hasVariablemessage()) { + event = EventMessage.SET_VARIABLE; + payload = subMessage.getVariablemessage(); } else { throw new Error("Unexpected batch message type"); } @@ -174,6 +177,7 @@ export class RoomConnection implements RoomConnection { } } } else if (message.hasRoomjoinedmessage()) { + console.error('COUCOU') const roomJoinedMessage = message.getRoomjoinedmessage() as RoomJoinedMessage; const items: { [itemId: number]: unknown } = {}; @@ -181,6 +185,11 @@ export class RoomConnection implements RoomConnection { items[item.getItemid()] = JSON.parse(item.getStatejson()); } + const variables = new Map(); + for (const variable of roomJoinedMessage.getVariableList()) { + variables.set(variable.getName(), JSON.parse(variable.getValue())); + } + this.userId = roomJoinedMessage.getCurrentuserid(); this.tags = roomJoinedMessage.getTagList(); @@ -188,6 +197,7 @@ export class RoomConnection implements RoomConnection { connection: this, room: { items, + variables, } as RoomJoinedMessageInterface, }); } else if (message.hasWorldfullmessage()) { @@ -634,6 +644,18 @@ export class RoomConnection implements RoomConnection { }); } + public onSetVariable(callback: (name: string, value: unknown) => void): void { + this.onMessage(EventMessage.SET_VARIABLE, (message: VariableMessage) => { + const name = message.getName(); + const serializedValue = message.getValue(); + let value: unknown = undefined; + if (serializedValue) { + value = JSON.parse(serializedValue); + } + callback(name, value); + }); + } + 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 1f326837..3ed0254b 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -707,7 +707,7 @@ export class GameScene extends DirtyScene { }); // Set up variables manager - this.sharedVariablesManager = new SharedVariablesManager(this.connection, this.gameMap); + this.sharedVariablesManager = new SharedVariablesManager(this.connection, this.gameMap, onConnect.room.variables); //this.initUsersPosition(roomJoinedMessage.users); this.connectionAnswerPromiseResolve(onConnect.room); diff --git a/front/src/Phaser/Game/SharedVariablesManager.ts b/front/src/Phaser/Game/SharedVariablesManager.ts index 284dec1d..c73ffac4 100644 --- a/front/src/Phaser/Game/SharedVariablesManager.ts +++ b/front/src/Phaser/Game/SharedVariablesManager.ts @@ -7,6 +7,7 @@ import type {Subscription} from "rxjs"; import type {GameMap} from "./GameMap"; import type {ITile, ITiledMapObject} from "../Map/ITiledMap"; import type {Var} from "svelte/types/compiler/interfaces"; +import {init} from "svelte/internal"; interface Variable { defaultValue: unknown, @@ -18,7 +19,7 @@ export class SharedVariablesManager { private _variables = new Map(); private variableObjects: Map; - constructor(private roomConnection: RoomConnection, private gameMap: GameMap) { + constructor(private roomConnection: RoomConnection, private gameMap: GameMap, serverVariables: Map) { // We initialize the list of variable object at room start. The objects cannot be edited later // (otherwise, this would cause a security issue if the scripting API can edit this list of objects) this.variableObjects = SharedVariablesManager.findVariablesInMap(gameMap); @@ -28,6 +29,22 @@ export class SharedVariablesManager { this._variables.set(name, variableObject.defaultValue); } + // Override default values with the variables from the server: + for (const [name, value] of serverVariables) { + this._variables.set(name, value); + } + + roomConnection.onSetVariable((name, value) => { + console.log('Set Variable received from server'); + this._variables.set(name, value); + + // On server change, let's notify the iframes + iframeListener.setVariable({ + key: name, + value: value, + }) + }); + // When a variable is modified from an iFrame iframeListener.registerAnswerer('setVariable', (event) => { const key = event.key; @@ -48,7 +65,8 @@ export class SharedVariablesManager { } this._variables.set(key, event.value); - // TODO: dispatch to the room connection. + + // Dispatch to the room connection. this.roomConnection.emitSetVariableEvent(key, event.value); }); } diff --git a/maps/tests/Variables/shared_variables.html b/maps/tests/Variables/shared_variables.html new file mode 100644 index 00000000..ae282b1c --- /dev/null +++ b/maps/tests/Variables/shared_variables.html @@ -0,0 +1,41 @@ + + + + + + + + + +
+ + diff --git a/maps/tests/index.html b/maps/tests/index.html index dbcf8287..aba4c41a 100644 --- a/maps/tests/index.html +++ b/maps/tests/index.html @@ -199,7 +199,15 @@ Success Failure Pending - Testing scripting variables + Testing scripting variables locally + + + + + Success Failure Pending + + + Testing shared scripting variables diff --git a/pusher/src/Model/PusherRoom.ts b/pusher/src/Model/PusherRoom.ts index 1eae7a9f..f0dd0e8f 100644 --- a/pusher/src/Model/PusherRoom.ts +++ b/pusher/src/Model/PusherRoom.ts @@ -13,6 +13,7 @@ import { } from "../Messages/generated/messages_pb"; import Debug from "debug"; import {ClientReadableStream} from "grpc"; +import {ExAdminSocketInterface} from "_Model/Websocket/ExAdminSocketInterface"; const debug = Debug("room"); @@ -34,8 +35,9 @@ export class PusherRoom { private backConnection!: ClientReadableStream; private isClosing: boolean = false; private listeners: Set = new Set(); + public readonly variables = new Map(); - constructor(public readonly roomId: string, private socketListener: ZoneEventListener, private onBackFailure: (e: Error | null, room: PusherRoom) => void) { + constructor(public readonly roomId: string, private socketListener: ZoneEventListener) { this.public = isRoomAnonymous(roomId); this.tags = []; this.policyType = GameRoomPolicyTypes.ANONYMOUS_POLICY; @@ -96,7 +98,11 @@ export class PusherRoom { for (const message of batch.getPayloadList()) { if (message.hasVariablemessage()) { const variableMessage = message.getVariablemessage() as VariableMessage; - // We need to dispatch this variable to all the listeners + + // We need to store all variables to dispatch variables later to the listeners + this.variables.set(variableMessage.getName(), variableMessage.getValue()); + + // Let's dispatch this variable to all the listeners for (const listener of this.listeners) { const subMessage = new SubMessage(); subMessage.setVariablemessage(variableMessage); @@ -112,14 +118,22 @@ export class PusherRoom { if (!this.isClosing) { debug("Error on back connection"); this.close(); - this.onBackFailure(e, this); + // Let's close all connections linked to that room + for (const listener of this.listeners) { + listener.disconnecting = true; + listener.end(1011, "Connection error between pusher and back server") + } } }); this.backConnection.on("close", () => { if (!this.isClosing) { debug("Close on back connection"); this.close(); - this.onBackFailure(null, this); + // Let's close all connections linked to that room + for (const listener of this.listeners) { + listener.disconnecting = true; + listener.end(1011, "Connection closed between pusher and back server") + } } }); } @@ -128,10 +142,5 @@ export class PusherRoom { debug("Closing connection to room %s on back server", this.roomId); this.isClosing = true; this.backConnection.cancel(); - - // Let's close all connections linked to that room - for (const listener of this.listeners) { - listener.close(); - } } } From 3cfbcc6b020b615e2b768295ff0d58c856ea7b0e Mon Sep 17 00:00:00 2001 From: kharhamel Date: Wed, 7 Jul 2021 18:07:58 +0200 Subject: [PATCH 022/101] FEATURE: migrated the chat window to svelte --- CHANGELOG.md | 5 + back/src/Model/GameRoom.ts | 6 - back/src/Services/SocketManager.ts | 6 - front/dist/index.tmpl.html | 3 +- front/dist/static/images/send.png | Bin 0 -> 8523 bytes front/src/Components/App.svelte | 13 +- front/src/Components/Chat/Chat.svelte | 97 ++++++++ front/src/Components/Chat/ChatElement.svelte | 74 ++++++ .../Components/Chat/ChatMessageForm.svelte | 55 +++++ .../src/Components/Chat/ChatPlayerName.svelte | 37 +++ front/src/Phaser/Components/OpenChatIcon.ts | 12 +- .../Entity/PlayerTexturesLoadingManager.ts | 1 - front/src/Phaser/Game/GameScene.ts | 4 +- front/src/Phaser/Game/PlayerInterface.ts | 1 + front/src/Stores/ChatStore.ts | 102 ++++++++ front/src/Stores/PlayersStore.ts | 2 + front/src/Stores/UserInputStore.ts | 13 +- front/src/WebRtc/ColorGenerator.ts | 48 ++++ front/src/WebRtc/DiscussionManager.ts | 226 +----------------- front/src/WebRtc/MediaManager.ts | 21 +- front/src/WebRtc/SimplePeer.ts | 17 +- front/src/WebRtc/VideoPeer.ts | 27 ++- front/style/fonts.scss | 4 - front/webpack.config.ts | 1 - 24 files changed, 470 insertions(+), 305 deletions(-) create mode 100644 front/dist/static/images/send.png create mode 100644 front/src/Components/Chat/Chat.svelte create mode 100644 front/src/Components/Chat/ChatElement.svelte create mode 100644 front/src/Components/Chat/ChatMessageForm.svelte create mode 100644 front/src/Components/Chat/ChatPlayerName.svelte create mode 100644 front/src/Stores/ChatStore.ts create mode 100644 front/src/WebRtc/ColorGenerator.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index a83e8213..018848b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,11 @@ - Use `WA.ui.registerMenuCommand(): void` to add a custom menu - Use `WA.room.setTiles(): void` to change an array of tiles - Users blocking now relies on UUID rather than ID. A blocked user that leaves a room and comes back will stay blocked. +- The text chat was redesigned to be prettier and to use more features : + - The chat is now persistent bewteen discussions and always accesible + - The chat now tracks incoming and outcoming users in your conversation + - The chat allows your to see the visit card of users + - You can close the chat window with the escape key ## Version 1.4.3 - 1.4.4 - 1.4.5 diff --git a/back/src/Model/GameRoom.ts b/back/src/Model/GameRoom.ts index 020f4c29..71d2124e 100644 --- a/back/src/Model/GameRoom.ts +++ b/back/src/Model/GameRoom.ts @@ -15,12 +15,6 @@ import { Admin } from "../Model/Admin"; export type ConnectCallback = (user: User, group: Group) => void; export type DisconnectCallback = (user: User, group: Group) => void; -export enum GameRoomPolicyTypes { - ANONYMOUS_POLICY = 1, - MEMBERS_ONLY_POLICY, - USE_TAGS_POLICY, -} - export class GameRoom { private readonly minDistance: number; private readonly groupRadius: number; diff --git a/back/src/Services/SocketManager.ts b/back/src/Services/SocketManager.ts index 8d04e713..8d1659df 100644 --- a/back/src/Services/SocketManager.ts +++ b/back/src/Services/SocketManager.ts @@ -436,10 +436,7 @@ export class SocketManager { const serverToClientMessage1 = new ServerToClientMessage(); serverToClientMessage1.setWebrtcstartmessage(webrtcStartMessage1); - //if (!user.socket.disconnecting) { user.socket.write(serverToClientMessage1); - //console.log('Sending webrtcstart initiator to '+user.socket.userId) - //} const webrtcStartMessage2 = new WebRtcStartMessage(); webrtcStartMessage2.setUserid(user.id); @@ -453,10 +450,7 @@ export class SocketManager { const serverToClientMessage2 = new ServerToClientMessage(); serverToClientMessage2.setWebrtcstartmessage(webrtcStartMessage2); - //if (!otherUser.socket.disconnecting) { otherUser.socket.write(serverToClientMessage2); - //console.log('Sending webrtcstart to '+otherUser.socket.userId) - //} } } diff --git a/front/dist/index.tmpl.html b/front/dist/index.tmpl.html index aa63229f..30ea8353 100644 --- a/front/dist/index.tmpl.html +++ b/front/dist/index.tmpl.html @@ -37,8 +37,7 @@
-
-
+
diff --git a/front/dist/static/images/send.png b/front/dist/static/images/send.png new file mode 100644 index 0000000000000000000000000000000000000000..1f75634a8ec2156ec4dfdf51353aa7f6f760143e GIT binary patch literal 8523 zcmcIqhhI}!u%-wo1aLtyAcANDq6DJ!7E}bhMqvr4NN52YH7FfITQ!Xg7H{O4KKtA_#t`vss=C+tJhCVRr(zBnH?GCQ3g zn}w9vdFr@elFB{V7w^nH@5!CHg%sP*jXfkYc--U1aO*o?kG;g!oOqgV$peG`G!NEY zF#lF(k#<5+{FJTyt_NWR>dBnHQdSe4)pl9*lrNc%ge^ratwou6PexCN@k*XYg_tc> zj>MoK`v2p9!`kP|1t5afgmjv~`q82zMKWdL%ThBnv3tKfR)q)^zXa|IZwlp!w4W=v zAfznxPQ&gc3YzScGLSE$kDB}2ja#v;MaCYZ)LWz2^|8Y`68}netgz+YOC+N_33hA} zd(&gyqgi#l=TgXisAbzb5o@7>{i@saokE1;=6o&a%wIe~Ymuq_xb6B*g$<=D5Iw~~ zC`~f&WzT-ae4CN)3>2i@B^r9rUq0-lKW8x=vGx$|W{Go+Y9yYD zf@I&2G>m$T-rtEjO&7haBex+xZ0_4#s@RDuKT2nkLU(y;gc}bF`?dr*`jb(Ra1nA@ zF(|=8zqDOw?QZaCdMh!z_&55IbkXl1KetR2WZL$s_;=uVQIQ`+55J}(7a21b+xDwm zM!EC>%J;IMzmC7k@U6ogM(WG}I@6`|v5s6-{G8z90F2l>iQk#1+?(d|an>xHUYGnP)KlCIQOTc#DqRc3k~i5u{%`EUwD|4BQc)#>4!S>y5Yb2tZ(^y&bHV?gngDC z%@T){O`M%B3fOssT%@;#C8FXj3SUuv2ZmMM=oZD(%YBdf8x{RENBXuHq*l}r>dQa{ zHDA{V#|@jme7Tr(BLxGEWjLSqXdXYLtXZ0`%f>YjHK8&3Ey$yJ?U1rYX+fdxR5>bF z_Lq>A(1}nB1&$|Yd`_?%oL|C7IJC%~BES#B@>Z-Vor?j4=znpog}Y z=A~VA1+RnH7hmQ&(;qeSn8m-z>dJ_7?Ze~ay1CgYYnFe?$lTxo|C$pSgo~1p0A%1KAD? zHT0t{31#l!KfQalj10*vCRii`D57_@-gP1^o3yYlpW66}pS6{MQa%PVS);C&EB@a9 zLvtjkzk+3#HY%3E!@9tRrMNuXP1dA?>>r!+M7*s(tANKBKU%{}rb z)yLb-V>(1@>H==9sz6o67*CB={O7yGhd~GZ#f$7H_?PDoBLt&lmKBmMbASB*pq(@EAPE4iS$XL34Jt^sQyN zj0C$4+}?P=^lAgf(SUKMDEO=SfygQAiEO#)%pzsF{fo$)0F82lLBhI6bNpi|^V3b@ zmR0SC`Ibrv^2-RG{nSk6k~cO@~$ zCeKxQG|P_i&FY*bIHt6!m6}2lKqLS->pa-74&VOSF`mS<3`WOWkh3Xex|c3KsLLYOvzT^XJIrJD zh;}T#j!8jzS|(Uj6u!i6Gte(8AVPK$kk z(`D?FQKsAiwsFkftto{a9aV>_(die6$djwae)hgNFBNsCc#q~%gtta{u>!}R2(U0X zQltk7>nX&!9EXz}xLFuL5w8HC_}Q`3TdCbAT#wivOx@X3ULDWz_i-ByERts8FbO~<*i{B?hZC=uxNLpm;ped$JJ=aP~wI^?C3n$>} z%4LRqArHH4`IKA!(nBR_I`i&R8}Wzphm?KJwV!FpRsviAkrz}Dtb>sWKK50azQq@y z#|ofyZLOeB$2nhqwai#JqYRvdsns>h9}Y*K|Eb<;DCcasI_FlOr9F;EHmaCv%ATJW z!E_wS6$&_=`H?X0umH_sM)Rw0tFYs4jpOvp8|X$}e~i9~D|i9BP(P_ID-mp6xTua$ z;q58g3q@-s>tJnE<1MDQCS(`qZ2IeVu&^c%7Q#wwOKXhndgw;a+Ci$azq7RmiN}zU zAfq;SJFJ~)v64D7_I?tie;;9s2x9GK$3s-x>akAS)>Vr5HrMi;34&q|s`oY)|Mp)z ziV*CTvtPSzs!Lv>-|Y0)P*_=d{I27=#%kU&198EXK7CQQ5(VCU;iCE2iH({d?XUR83*E@=)ZTFGyqt}iHAcZIq4Bf`7 zD>--U-y5ak%3OahmaDTKjkeQt8x0^!)T^Ab+UKcEn-f0^q3TSxmF&suES; z+F82}18?-`Jiz+08K~7RXMyB>GEMK;ZESaJXUN&irz{FI1d3<*)yRefi+TBzNTJGy zou-&Zu;R;i2YX&?ZaFZ7?pK^h1A-ohNCwHBX1gi+!043Dx>^lmTwBhrV&&4sr~kg{aT<_==^!si7-U0G9l+?wR*vo&m8-Z&!MC#i z`*-FOcWN>{>`U@n^)+lXM944htqN zBUwgrlT;PzKqFgxkQZXvJBJqZ0Y#Bd-(s$FtAqW*gT$;Ih@%LVNKf)lrOZbxif5;i zl|o$a2{`czKzqdkhNk$B z0f0%GnTa)oB}r3+AkM1WC9?IT_&<`MYD#PYA`Sj;8Q5zYPn@PT*>kgYVwt4P(&$DO8TN69OM=vD(cXR)+IkLDD4>L*IewRp?Pl~e%J7z^#M z=&wF}EB}+@iB;m46SEjwOX|Qa7mJB%S2~N5>k?rHqQ*-xj(3N&{HNIgzrLCeG@4F! zvqRu+I9fppM`pZMMsV(iixx*jdjm_t@7HCSy`mK9UOEDK*!Cg+)1nS=A~QOxj87!# z{j=9!e8zaoly9r$%7669?7oV`b!W${(YiL(<{4T+#C(tDE6mh9kLIXj2>r>LKe}$I z{lvJAWr#4bEAm^KLgLfWd-`O8?vctoC1Qy8#%sfXE#+50?~uOb*UIWCP@%IDjv`(y z?cwqUKET7Iqoa~)pnn{xXPw%28R^%XNE_$qF6Z2Vj|Ek?=W0Ru1u2Hpu11exIsWq* z=X|t!VGQnD!I>l1!r82VMXwGAX z016Yi2e_o+lQ6@Aa)~ppM!&vCGBBQ2d#4ulvM~J0MUahpTVNak#mI_+x0mPm2g^O+ z*U-j3>CZ8=N?!pcvH(HxFYx(E>cGT48y{B}5Tokt|5tPXzk2m6x_JWGqnx6rSGbGA z`s_5oC`W_QkwLaUlVg54uwWJhL45ccQg6PV%llObfIe3MmbL+8mJbp@35!TwQyUBq zkS>Y@dd~tE?)ekfnUbe#AlLR4!TF_RKB0!+a0X+)lps)*wqk7x?!MdTG^ z!t^QkwZKpyJUV$Y-J`hz=v4*u%Fsrr$*!2bsHp>J3yQ%9gRR!|2uQzt;9)^5LT%o} zJtF8P5EW}si??XXh9l3ry@P^Eu+YVE{BOFd6kPlQy5`+jptlvABU7q9?vY|Cs#lQK zel{&?CKik1Z!3qre`mm3AAtm`L48)(&%xI9$#fK!viV_OI(9aw#LMlAJ3}Qs%8%)j z?I;PDXB0labSk{$Q|VR!7>>$CxKZ#r{!~(${?0BIXAX>q9#j8wYOBuFsDQM*N2Xmy zimn;nh^zqT#Xp~cK6(7KG+$ZrtK{_GkC4=2DfGJ#H9(A!rKTXyiT_)fN9EQ)#-ElZ zA?u9qmi2;zMwy7;v;WxqOeein?m-GjeS;iSQD|0}g>0I7M%2 zyzWMDf+_dQ`1v9{AnTx1ACr2%VTSIm;0IBEo#XOm?|>=!F6Y*21}qE7f?6Mnb3PwMbZJ|xMm6W) zT&T)r&o3F!!ugh=%KoA=+zUo34H=})0?xh3;Zy*N0!HwHE$;VK#}2{iZNXlzBnbzp zq_swLL2^hEUrGI(w|Lp@ITGvrqG|sjHy6C=gOp>4E|zj}q9a9zyz;tvUR^mf)$vG7 z^!`XYYAc#bQc_kc$n-wlbnHxOkisyB%n8!=KU_piO=Z;m%-$X5gI`r@$;pVq0O7Wf zL&|@R$4JDO3IAeMID7i^BIXUBc2+B;uqbVZ=e`Z4f^)^)-b?F0U(p^Jb5xTK&^nHE6OuY7(=H}HWefsyt zC)3&OFp@6N`{O#uwkuRp)Y>0wf-1_Obb7IL{UwIYflz+4>ozH`p!$}tiVqpHI|f%P z6grK`D`U4hY;g!l%b+_Xhq?pqP6ZE!B4&(T`b_#qX4U19#Y&oyk+MH@_#Fq#Y7-cMZMM2?~ZRBNSFlUsz*ky%z6c5I+hzgQezchDo z{R>EAgk@U;b|Qf#+ZEFHmg)XWz)JYypSW$3mJL)==e_`1q}AFBK(0n?H+xA0$1lvV zxXMfgRL5WP_s0%%iXslwq^(Q2y#Y*B;(>jQM{uSL?{!*Xa1Q|E8s4&fAy4fIRK2m` zDmei(_oQUIRi@3UT*pe?C3T)l6qq_Vg{X#kkXoDL1Z2ba6pzewgT;ywmZhJt69r-H zAZ|M61yJ;d2lj9ZsnfT9XM|k+2*%Ybvo>ctm9&&@(|xI1O5-cB`ccy%q*gX%yTbjT zqlk)Wb#6xe8equBB<$-QoFb`Al_`@8f{DOy_Kez`r$}m0r@X_7?xz&Ew1yz>|JcPT zQrf)J8E^@%77>=U%b%H0?#5$`+*-fURb zh8~6eo0dj$c;y`Pn48|b2h@wjxLOA@PLa#2oe|bepzCY35%ow%aKe2ByU%tTIs?FR zH(z3r-XvuEWE(f5?FPVy>apwo9$Q$AyV{8HrGtZwpRF)V1fp*0D0n>U-ZWkX&uVa+ z5akppS0C>VGZmP~0DYkwmT?)$T zS-5FEVTdyYJ?M|CSlmbO;tqR~n?3}HEBVnkLVx`u=~_bVbtJEA2RCE*2&}AH{?wqICfW|S%BJ)-=R*|YD8Q z#j;5ThzOxK_l&FJ69A&qE@ ziSJ6OK<`qczotZo6#CyVi@b~x(of$ei60elI@A5Vcl+0-a$%^AD8Bw+PUT5NnDzu? zL^h&&8t%Yx@&v2`wP#*;urUNZ^AT({mR$|B=`@WHd_yxHvH?yZ^kD?6%~u4p5o^;2 zxf%0zAhJCVF$DTvQl7{I=yuOU3^uN;0bq1a>B6$kWE3wi5k7LLyukoFrc#8WEV?hP z<=kN&0Ag8`etGI*xJvQJMgiEc#XLwCp0rlOOq;Ii_&6jSU-x=x>Um7u1l7Lv4XG-_o-+V$oqQDv8W-J*4v!Gdqz~f$E@(&i*%v6G&p8lv~ewNW`WyjvlO&>31SLPW%U zSV)lXm024x8EpJ!g+WOIE%*jI9q`>@8E~o`_Era@gAYNl$-;^bhvD=34gfAqAJT`P zd-^fOQDig^0XN;ySi6y3XmlK%Wa*Xx>CGs8rCH5}6jX3iu#c ztH=YkGNKaQJ~9hm>wJQB2?6VWk>~PdhQgqha4j~`9TU7i-XObitP&nx0>d0*+Q#rO z%uLLwd2nLQ@Q6Hht_TrkJQCn74oGY;_XQ^mPX*6hA@VJUbCqc?bY$uTQ4+ zV5Q44%1V|cv1;Q|;7S~I-e)`(!08CJ_>je|{3Wcv zvt!zIC|i!c>z7q1{_F5#E??*-=o`_weF*>{&7^~4!yv9ucK{mIAGrL%8;)Sm6IqFV zH$Bf?48asd!_NlbdBm2z!ChQ}>rG2h<#sa7H%zOz$`lS19g|*}A((ERTno7Xt=u5+ zywA$V#s<(aO0pwymVs3NC-Y0}B&<0n2-Pc-ZIasQ5(NvR$h4#Uq%vH=R-cCYL>&AX zEUdJ@;O+?Q1)V|6jbqW~6I#2q;5hdD`5_dq*df#}Hy9owf_)q$25wzu7zP<~MP{tn}B^B_k@z^#pt-&O@)AaxjjcUy0>xaZbn-Ph&@09f=CFzo57@=h%g^fM?=MXe9SK`L+C1=)_o z!Yvau$u#Ljl6&-tP!|9;^l*mp#7AU3qYSq$1ir8^U!N5O8_q6r`R@Mi@Pt2Hs4Q-# zENjyk4&2u*|B-2TW!5BW#o$o-TjwWn&`g#!-Nx>P?Xgf8!`y&x8kb?WL%yMgoC!fPjZ-HAmU`%jho(Y+N}kR zC1wBK#LQydmzHB;({3XF#mqVl@>nSge&->A^x_JmOiP`a&Evh;8csRDOC72R#IG9^ zlQ!YVq2Dx4;rGa}M5a{zY?5Jw?N2I`X=GKspNVkT=03vp$h?aA@+#LmEy%bC3sMF8 zumDuzYIvtjhQq-Kz1U}N=11$h{^!tH@7*)M;Z@J19^gp?;=5&*gxf`1agMH_!9nF7 z;omPO(=xZ`MeZv?!R8b^F|T5AE!d{9rLJkYeh|T`EgVn5`}Y)nvS*=kv%Nqp2o{Lv zt?0RZVR$o8=ldPT%F%pJ|Eg*4_bw0t?Nx#1d%NYoi7|D|J5RTN>gY!klUd98u*dPW z9o;_HkSSd{wThWtU@L$RF<#FtA;K^}&96@;M1a53q0gdr5qv+(8fS4?V(ssw3^c)Y zC!r~Dkzuf>%&16n>ucJDi)5OXM4soqnO<=53Yjj6mxtijt=&we1uDc^QP9}?f*u*^ z13cTfCYwLwP@Zn(09|{3Ow*HRa!?EVtB7XyI_8E|e=pmJ5pR17<=O87tq=#J#R~?` zd$jI{6v|pyT+k=R7DgW_dPb5)K|-H}7XL@C7l$XoFl@yfH`Q~FZ zo^oc!J42T@g@zmpxtQ5+v4t$D&{dS=1C%Ff1asq>n?=93v%xGpPS*4{4D+K+OHtB; zn9}kOQ0iabcy2Nbl*@24x_(0M?s;C^e6fH zIntTuR>1vIKXv@uq~AsGlq%tLVg-D)ef6%w8KS>jQur)3VAQfl98b4Dd(n?p|F+5& zqF*JfGJXR-Wm+GK#8WP=5g(yEgI)08jI?8dPKu_Z8qVYr2cH`1sX+u;v>z?5Wa2EH zDf&z3B3%nnlX{IvN9l%`tO(8KoQ zEWp17K&c4`-_Ck~Kl9YXUk^Ahf_u-mxX2D`L7G$BGx&E+}l0$>or;sHO} zrglUd9aY+Ys7{(w5!*ADtN{^>8<}Cq0+9raZ5|5peJY*yVS?ko+uuFu3>x~}AH;6( z!6A|bn>5ZpYt^*_AFx|f?y&JO=CQ)DBJ6ZFntd^L`r%);|DV4Q06#yt!rK&dd&Y2y TvtN}2_6tB3)~5
{/if} - - {#if $gameOverlayVisibilityStore}
@@ -94,4 +88,7 @@
{/if} + {#if $chatVisibilityStore} + + {/if}
diff --git a/front/src/Components/Chat/Chat.svelte b/front/src/Components/Chat/Chat.svelte new file mode 100644 index 00000000..c4f9075d --- /dev/null +++ b/front/src/Components/Chat/Chat.svelte @@ -0,0 +1,97 @@ + + + + + + + + \ No newline at end of file diff --git a/front/src/Components/Chat/ChatElement.svelte b/front/src/Components/Chat/ChatElement.svelte new file mode 100644 index 00000000..95a050eb --- /dev/null +++ b/front/src/Components/Chat/ChatElement.svelte @@ -0,0 +1,74 @@ + + +
+
+ {#if message.type === ChatMessageTypes.userIncoming} + ➡️: {#each targets as target}{/each} ({renderDate(message.date)}) + {:else if message.type === ChatMessageTypes.userOutcoming} + ⬅️: {#each targets as target}{/each} ({renderDate(message.date)}) + {:else if message.type === ChatMessageTypes.me} +

Me: ({renderDate(message.date)})

+ {#each texts as text} +

{@html urlifyText(text)}

+ {/each} + {:else} +

: ({renderDate(message.date)})

+ {#each texts as text} +

{@html urlifyText(text)}

+ {/each} + {/if} +
+
+ + \ No newline at end of file diff --git a/front/src/Components/Chat/ChatMessageForm.svelte b/front/src/Components/Chat/ChatMessageForm.svelte new file mode 100644 index 00000000..acfd68ae --- /dev/null +++ b/front/src/Components/Chat/ChatMessageForm.svelte @@ -0,0 +1,55 @@ + + +
+ + +
+ + \ No newline at end of file diff --git a/front/src/Components/Chat/ChatPlayerName.svelte b/front/src/Components/Chat/ChatPlayerName.svelte new file mode 100644 index 00000000..f0fbe8cd --- /dev/null +++ b/front/src/Components/Chat/ChatPlayerName.svelte @@ -0,0 +1,37 @@ + + + showMenu = !showMenu}> + {player.name} + + +{#if showMenu} +
    +
  • +
+{/if} + + + \ No newline at end of file diff --git a/front/src/Phaser/Components/OpenChatIcon.ts b/front/src/Phaser/Components/OpenChatIcon.ts index ab07a80c..8c648bc1 100644 --- a/front/src/Phaser/Components/OpenChatIcon.ts +++ b/front/src/Phaser/Components/OpenChatIcon.ts @@ -1,7 +1,7 @@ -import {discussionManager} from "../../WebRtc/DiscussionManager"; -import {DEPTH_INGAME_TEXT_INDEX} from "../Game/DepthIndexes"; +import { DEPTH_INGAME_TEXT_INDEX } from "../Game/DepthIndexes"; +import { chatVisibilityStore } from "../../Stores/ChatStore"; -export const openChatIconName = 'openChatIcon'; +export const openChatIconName = "openChatIcon"; export class OpenChatIcon extends Phaser.GameObjects.Image { constructor(scene: Phaser.Scene, x: number, y: number) { super(scene, x, y, openChatIconName, 3); @@ -9,9 +9,9 @@ export class OpenChatIcon extends Phaser.GameObjects.Image { this.setScrollFactor(0, 0); this.setOrigin(0, 1); this.setInteractive(); - this.setVisible(false); + //this.setVisible(false); this.setDepth(DEPTH_INGAME_TEXT_INDEX); - this.on("pointerup", () => discussionManager.showDiscussionPart()); + this.on("pointerup", () => chatVisibilityStore.set(true)); } -} \ No newline at end of file +} diff --git a/front/src/Phaser/Entity/PlayerTexturesLoadingManager.ts b/front/src/Phaser/Entity/PlayerTexturesLoadingManager.ts index d2a659ec..3c47c9d9 100644 --- a/front/src/Phaser/Entity/PlayerTexturesLoadingManager.ts +++ b/front/src/Phaser/Entity/PlayerTexturesLoadingManager.ts @@ -101,7 +101,6 @@ export const createLoadingPromise = ( frameConfig: FrameConfig ) => { return new Promise((res, rej) => { - console.log("count", loadPlugin.listenerCount("loaderror")); if (loadPlugin.textureManager.exists(playerResourceDescriptor.name)) { return res(playerResourceDescriptor); } diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index d6df242f..3ccc9fd9 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -692,12 +692,12 @@ export class GameScene extends DirtyScene { const self = this; this.simplePeer.registerPeerConnectionListener({ onConnect(peer) { - self.openChatIcon.setVisible(true); + //self.openChatIcon.setVisible(true); audioManager.decreaseVolume(); }, onDisconnect(userId: number) { if (self.simplePeer.getNbConnections() === 0) { - self.openChatIcon.setVisible(false); + //self.openChatIcon.setVisible(false); audioManager.restoreVolume(); } }, diff --git a/front/src/Phaser/Game/PlayerInterface.ts b/front/src/Phaser/Game/PlayerInterface.ts index 5a81c89a..6ab439df 100644 --- a/front/src/Phaser/Game/PlayerInterface.ts +++ b/front/src/Phaser/Game/PlayerInterface.ts @@ -7,4 +7,5 @@ export interface PlayerInterface { visitCardUrl: string | null; companion: string | null; userUuid: string; + color?: string; } diff --git a/front/src/Stores/ChatStore.ts b/front/src/Stores/ChatStore.ts new file mode 100644 index 00000000..344a424e --- /dev/null +++ b/front/src/Stores/ChatStore.ts @@ -0,0 +1,102 @@ +import { writable } from "svelte/store"; +import { playersStore } from "./PlayersStore"; +import type { PlayerInterface } from "../Phaser/Game/PlayerInterface"; + +export const chatVisibilityStore = writable(false); +export const chatInputFocusStore = writable(false); + +export const newChatMessageStore = writable(null); + +export enum ChatMessageTypes { + text = 1, + me, + userIncoming, + userOutcoming, +} + +export interface ChatMessage { + type: ChatMessageTypes; + date: Date; + author?: PlayerInterface; + targets?: PlayerInterface[]; + text?: string[]; +} + +function getAuthor(authorId: number): PlayerInterface { + const author = playersStore.getPlayerById(authorId); + if (!author) { + throw "Could not find data for author " + authorId; + } + return author; +} + +function createChatMessagesStore() { + const { subscribe, update } = writable([]); + + return { + subscribe, + addIncomingUser(authorId: number) { + update((list) => { + const lastMessage = list[list.length - 1]; + if (lastMessage && lastMessage.type === ChatMessageTypes.userIncoming && lastMessage.targets) { + lastMessage.targets.push(getAuthor(authorId)); + } else { + list.push({ + type: ChatMessageTypes.userIncoming, + targets: [getAuthor(authorId)], + date: new Date(), + }); + } + return list; + }); + }, + addOutcomingUser(authorId: number) { + update((list) => { + const lastMessage = list[list.length - 1]; + if (lastMessage && lastMessage.type === ChatMessageTypes.userOutcoming && lastMessage.targets) { + lastMessage.targets.push(getAuthor(authorId)); + } else { + list.push({ + type: ChatMessageTypes.userOutcoming, + targets: [getAuthor(authorId)], + date: new Date(), + }); + } + return list; + }); + }, + addPersonnalMessage(text: string) { + newChatMessageStore.set(text); + update((list) => { + const lastMessage = list[list.length - 1]; + if (lastMessage && lastMessage.type === ChatMessageTypes.me && lastMessage.text) { + lastMessage.text.push(text); + } else { + list.push({ + type: ChatMessageTypes.me, + text: [text], + date: new Date(), + }); + } + return list; + }); + }, + addExternalMessage(authorId: number, text: string) { + update((list) => { + const lastMessage = list[list.length - 1]; + if (lastMessage && lastMessage.type === ChatMessageTypes.text && lastMessage.text) { + lastMessage.text.push(text); + } else { + list.push({ + type: ChatMessageTypes.text, + text: [text], + author: getAuthor(authorId), + date: new Date(), + }); + } + return list; + }); + }, + }; +} +export const chatMessagesStore = createChatMessagesStore(); diff --git a/front/src/Stores/PlayersStore.ts b/front/src/Stores/PlayersStore.ts index 6c21de7a..2ea988bb 100644 --- a/front/src/Stores/PlayersStore.ts +++ b/front/src/Stores/PlayersStore.ts @@ -1,6 +1,7 @@ import { writable } from "svelte/store"; import type { PlayerInterface } from "../Phaser/Game/PlayerInterface"; import type { RoomConnection } from "../Connexion/RoomConnection"; +import { getRandomColor } from "../WebRtc/ColorGenerator"; /** * A store that contains the list of players currently known. @@ -24,6 +25,7 @@ function createPlayersStore() { visitCardUrl: message.visitCardUrl, companion: message.companion, userUuid: message.userUuid, + color: getRandomColor(), }); return users; }); diff --git a/front/src/Stores/UserInputStore.ts b/front/src/Stores/UserInputStore.ts index cbb7f0c3..993d8795 100644 --- a/front/src/Stores/UserInputStore.ts +++ b/front/src/Stores/UserInputStore.ts @@ -1,10 +1,11 @@ -import {derived} from "svelte/store"; -import {consoleGlobalMessageManagerFocusStore} from "./ConsoleGlobalMessageManagerStore"; +import { derived } from "svelte/store"; +import { consoleGlobalMessageManagerFocusStore } from "./ConsoleGlobalMessageManagerStore"; +import { chatInputFocusStore } from "./ChatStore"; //derived from the focus on Menu, ConsoleGlobal, Chat and ... export const enableUserInputsStore = derived( - consoleGlobalMessageManagerFocusStore, - ($consoleGlobalMessageManagerFocusStore) => { - return !$consoleGlobalMessageManagerFocusStore; + [consoleGlobalMessageManagerFocusStore, chatInputFocusStore], + ([$consoleGlobalMessageManagerFocusStore, $chatInputFocusStore]) => { + return !$consoleGlobalMessageManagerFocusStore && !$chatInputFocusStore; } -); \ No newline at end of file +); diff --git a/front/src/WebRtc/ColorGenerator.ts b/front/src/WebRtc/ColorGenerator.ts new file mode 100644 index 00000000..a42aee85 --- /dev/null +++ b/front/src/WebRtc/ColorGenerator.ts @@ -0,0 +1,48 @@ +export function getRandomColor(): string { + return hsv_to_rgb(Math.random(), 0.5, 0.95); +} + +//todo: test this. +function hsv_to_rgb(hue: number, saturation: number, brightness: number): string { + const h_i = Math.floor(hue * 6); + const f = hue * 6 - h_i; + const p = brightness * (1 - saturation); + const q = brightness * (1 - f * saturation); + const t = brightness * (1 - (1 - f) * saturation); + let r: number, g: number, b: number; + switch (h_i) { + case 0: + r = brightness; + g = t; + b = p; + break; + case 1: + r = q; + g = brightness; + b = p; + break; + case 2: + r = p; + g = brightness; + b = t; + break; + case 3: + r = p; + g = q; + b = brightness; + break; + case 4: + r = t; + g = p; + b = brightness; + break; + case 5: + r = brightness; + g = p; + b = q; + break; + default: + throw "h_i cannot be " + h_i; + } + return "#" + Math.floor(r * 256).toString(16) + Math.floor(g * 256).toString(16) + Math.floor(b * 256).toString(16); +} diff --git a/front/src/WebRtc/DiscussionManager.ts b/front/src/WebRtc/DiscussionManager.ts index ae351f76..a3c928f4 100644 --- a/front/src/WebRtc/DiscussionManager.ts +++ b/front/src/WebRtc/DiscussionManager.ts @@ -1,232 +1,12 @@ -import { HtmlUtils } from "./HtmlUtils"; -import type { UserInputManager } from "../Phaser/UserInput/UserInputManager"; -import { connectionManager } from "../Connexion/ConnectionManager"; -import { GameConnexionTypes } from "../Url/UrlManager"; import { iframeListener } from "../Api/IframeListener"; -import { showReportScreenStore } from "../Stores/ShowReportScreenStore"; - -export type SendMessageCallback = (message: string) => void; +import { chatMessagesStore, chatVisibilityStore } from "../Stores/ChatStore"; export class DiscussionManager { - private mainContainer: HTMLDivElement; - - private divDiscuss?: HTMLDivElement; - private divParticipants?: HTMLDivElement; - private nbpParticipants?: HTMLParagraphElement; - private divMessages?: HTMLParagraphElement; - - private participants: Map = new Map(); - - private activeDiscussion: boolean = false; - - private sendMessageCallBack: Map = new Map< - number | string, - SendMessageCallback - >(); - - private userInputManager?: UserInputManager; - constructor() { - this.mainContainer = HtmlUtils.getElementByIdOrFail("main-container"); - this.createDiscussPart(""); //todo: why do we always use empty string? - iframeListener.chatStream.subscribe((chatEvent) => { - this.addMessage(chatEvent.author, chatEvent.message, false); - this.showDiscussion(); + chatMessagesStore.addExternalMessage(parseInt(chatEvent.author), chatEvent.message); + chatVisibilityStore.set(true); }); - this.onSendMessageCallback("iframe_listener", (message) => { - iframeListener.sendUserInputChat(message); - }); - } - - private createDiscussPart(name: string) { - this.divDiscuss = document.createElement("div"); - this.divDiscuss.classList.add("discussion"); - - const buttonCloseDiscussion: HTMLButtonElement = document.createElement("button"); - buttonCloseDiscussion.classList.add("close-btn"); - buttonCloseDiscussion.innerHTML = ``; - buttonCloseDiscussion.addEventListener("click", () => { - this.hideDiscussion(); - }); - this.divDiscuss.appendChild(buttonCloseDiscussion); - - const myName: HTMLParagraphElement = document.createElement("p"); - myName.innerText = name.toUpperCase(); - this.nbpParticipants = document.createElement("p"); - this.nbpParticipants.innerText = "PARTICIPANTS (1)"; - - this.divParticipants = document.createElement("div"); - this.divParticipants.classList.add("participants"); - - this.divMessages = document.createElement("div"); - this.divMessages.classList.add("messages"); - this.divMessages.innerHTML = "

Local messages

"; - - this.divDiscuss.appendChild(myName); - this.divDiscuss.appendChild(this.nbpParticipants); - this.divDiscuss.appendChild(this.divParticipants); - this.divDiscuss.appendChild(this.divMessages); - - const sendDivMessage: HTMLDivElement = document.createElement("div"); - sendDivMessage.classList.add("send-message"); - const inputMessage: HTMLInputElement = document.createElement("input"); - inputMessage.onfocus = () => { - if (this.userInputManager) { - this.userInputManager.disableControls(); - } - }; - inputMessage.onblur = () => { - if (this.userInputManager) { - this.userInputManager.restoreControls(); - } - }; - inputMessage.type = "text"; - inputMessage.addEventListener("keyup", (event: KeyboardEvent) => { - if (event.key === "Enter") { - event.preventDefault(); - if (inputMessage.value === null || inputMessage.value === "" || inputMessage.value === undefined) { - return; - } - this.addMessage(name, inputMessage.value, true); - for (const callback of this.sendMessageCallBack.values()) { - callback(inputMessage.value); - } - inputMessage.value = ""; - } - }); - sendDivMessage.appendChild(inputMessage); - this.divDiscuss.appendChild(sendDivMessage); - - //append in main container - this.mainContainer.appendChild(this.divDiscuss); - - this.addParticipant("me", "Moi", undefined, true); - } - - public addParticipant( - userId: number | "me", - name: string | undefined, - img?: string | undefined, - isMe: boolean = false - ) { - const divParticipant: HTMLDivElement = document.createElement("div"); - divParticipant.classList.add("participant"); - divParticipant.id = `participant-${userId}`; - - const divImgParticipant: HTMLImageElement = document.createElement("img"); - divImgParticipant.src = "resources/logos/boy.svg"; - if (img !== undefined) { - divImgParticipant.src = img; - } - const divPParticipant: HTMLParagraphElement = document.createElement("p"); - if (!name) { - name = "Anonymous"; - } - divPParticipant.innerText = name; - - divParticipant.appendChild(divImgParticipant); - divParticipant.appendChild(divPParticipant); - - if ( - !isMe && - connectionManager.getConnexionType && - connectionManager.getConnexionType !== GameConnexionTypes.anonymous && - userId !== "me" - ) { - const reportBanUserAction: HTMLButtonElement = document.createElement("button"); - reportBanUserAction.classList.add("report-btn"); - reportBanUserAction.innerText = "Report"; - reportBanUserAction.addEventListener("click", () => { - showReportScreenStore.set({ userId: userId, userName: name ? name : "" }); - }); - divParticipant.appendChild(reportBanUserAction); - } - - this.divParticipants?.appendChild(divParticipant); - - this.participants.set(userId, divParticipant); - - this.updateParticipant(this.participants.size); - } - - public updateParticipant(nb: number) { - if (!this.nbpParticipants) { - return; - } - this.nbpParticipants.innerText = `PARTICIPANTS (${nb})`; - } - - public addMessage(name: string, message: string, isMe: boolean = false) { - const divMessage: HTMLDivElement = document.createElement("div"); - divMessage.classList.add("message"); - if (isMe) { - divMessage.classList.add("me"); - } - - const pMessage: HTMLParagraphElement = document.createElement("p"); - const date = new Date(); - if (isMe) { - name = "Me"; - } else { - name = HtmlUtils.escapeHtml(name); - } - pMessage.innerHTML = `${name} - - ${date.getHours()}:${date.getMinutes()} - `; - divMessage.appendChild(pMessage); - - const userMessage: HTMLParagraphElement = document.createElement("p"); - userMessage.innerHTML = HtmlUtils.urlify(message); - userMessage.classList.add("body"); - divMessage.appendChild(userMessage); - this.divMessages?.appendChild(divMessage); - - //automatic scroll when there are new message - setTimeout(() => { - this.divMessages?.scroll({ - top: this.divMessages?.scrollTop + divMessage.getBoundingClientRect().y, - behavior: "smooth", - }); - }, 200); - } - - public removeParticipant(userId: number | string) { - const element = this.participants.get(userId); - if (element) { - element.remove(); - this.participants.delete(userId); - } - //if all participant leave, hide discussion button - - this.sendMessageCallBack.delete(userId); - } - - public onSendMessageCallback(userId: string | number, callback: SendMessageCallback): void { - this.sendMessageCallBack.set(userId, callback); - } - - get activatedDiscussion() { - return this.activeDiscussion; - } - - private showDiscussion() { - this.activeDiscussion = true; - this.divDiscuss?.classList.add("active"); - } - - private hideDiscussion() { - this.activeDiscussion = false; - this.divDiscuss?.classList.remove("active"); - } - - public setUserInputManager(userInputManager: UserInputManager) { - this.userInputManager = userInputManager; - } - - public showDiscussionPart() { - this.showDiscussion(); } } diff --git a/front/src/WebRtc/MediaManager.ts b/front/src/WebRtc/MediaManager.ts index d9847f44..126bf1a8 100644 --- a/front/src/WebRtc/MediaManager.ts +++ b/front/src/WebRtc/MediaManager.ts @@ -1,16 +1,11 @@ -import { DivImportance, layoutManager } from "./LayoutManager"; +import { layoutManager } from "./LayoutManager"; import { HtmlUtils } from "./HtmlUtils"; -import { discussionManager, SendMessageCallback } from "./DiscussionManager"; import type { UserInputManager } from "../Phaser/UserInput/UserInputManager"; -import { localUserStore } from "../Connexion/LocalUserStore"; -import type { UserSimplePeerInterface } from "./SimplePeer"; -import { SoundMeter } from "../Phaser/Components/SoundMeter"; import { DISABLE_NOTIFICATIONS } from "../Enum/EnvironmentVariable"; import { localStreamStore } from "../Stores/MediaStore"; import { screenSharingLocalStreamStore } from "../Stores/ScreenSharingStore"; import { helpCameraSettingsVisibleStore } from "../Stores/HelpCameraSettingsStore"; -export type UpdatedLocalStreamCallback = (media: MediaStream | null) => void; export type StartScreenSharingCallback = (media: MediaStream) => void; export type StopScreenSharingCallback = (media: MediaStream) => void; @@ -182,22 +177,8 @@ export class MediaManager { } } - public addNewMessage(name: string, message: string, isMe: boolean = false) { - discussionManager.addMessage(name, message, isMe); - - //when there are new message, show discussion - if (!discussionManager.activatedDiscussion) { - discussionManager.showDiscussionPart(); - } - } - - public addSendMessageCallback(userId: string | number, callback: SendMessageCallback) { - discussionManager.onSendMessageCallback(userId, callback); - } - public setUserInputManager(userInputManager: UserInputManager) { this.userInputManager = userInputManager; - discussionManager.setUserInputManager(userInputManager); } public getNotification() { diff --git a/front/src/WebRtc/SimplePeer.ts b/front/src/WebRtc/SimplePeer.ts index 5045a5a3..e30f1b1f 100644 --- a/front/src/WebRtc/SimplePeer.ts +++ b/front/src/WebRtc/SimplePeer.ts @@ -12,6 +12,7 @@ import { localStreamStore, LocalStreamStoreValue, obtainedMediaConstraintStore } import { screenSharingLocalStreamStore } from "../Stores/ScreenSharingStore"; import { discussionManager } from "./DiscussionManager"; import { playersStore } from "../Stores/PlayersStore"; +import { newChatMessageStore } from "../Stores/ChatStore"; export interface UserSimplePeerInterface { userId: number; @@ -155,27 +156,11 @@ export class SimplePeer { const name = this.getName(user.userId); - discussionManager.removeParticipant(user.userId); - this.lastWebrtcUserName = user.webRtcUser; this.lastWebrtcPassword = user.webRtcPassword; const peer = new VideoPeer(user, user.initiator ? user.initiator : false, name, this.Connection, localStream); - //permit to send message - mediaManager.addSendMessageCallback(user.userId, (message: string) => { - peer.write( - new Buffer( - JSON.stringify({ - type: MESSAGE_TYPE_MESSAGE, - name: this.myName.toUpperCase(), - userId: this.userId, - message: message, - }) - ) - ); - }); - peer.toClose = false; // When a connection is established to a video stream, and if a screen sharing is taking place, // the user sharing screen should also initiate a connection to the remote user! diff --git a/front/src/WebRtc/VideoPeer.ts b/front/src/WebRtc/VideoPeer.ts index bde0bcde..45118b5f 100644 --- a/front/src/WebRtc/VideoPeer.ts +++ b/front/src/WebRtc/VideoPeer.ts @@ -5,10 +5,11 @@ import type { RoomConnection } from "../Connexion/RoomConnection"; import { blackListManager } from "./BlackListManager"; import type { Subscription } from "rxjs"; import type { UserSimplePeerInterface } from "./SimplePeer"; -import { get, readable, Readable } from "svelte/store"; +import { get, readable, Readable, Unsubscriber } from "svelte/store"; import { obtainedMediaConstraintStore } from "../Stores/MediaStore"; import { discussionManager } from "./DiscussionManager"; import { playersStore } from "../Stores/PlayersStore"; +import { chatMessagesStore, chatVisibilityStore, newChatMessageStore } from "../Stores/ChatStore"; const Peer: SimplePeerNamespace.SimplePeer = require("simple-peer"); @@ -34,6 +35,7 @@ export class VideoPeer extends Peer { public readonly streamStore: Readable; public readonly statusStore: Readable; public readonly constraintsStore: Readable; + private newMessageunsubscriber: Unsubscriber | null = null; constructor( public user: UserSimplePeerInterface, @@ -147,6 +149,20 @@ export class VideoPeer extends Peer { this.on("connect", () => { this._connected = true; + chatMessagesStore.addIncomingUser(this.userId); + + this.newMessageunsubscriber = newChatMessageStore.subscribe((newMessage) => { + if (!newMessage) return; + this.write( + new Buffer( + JSON.stringify({ + type: MESSAGE_TYPE_MESSAGE, + message: newMessage, + }) + ) + ); //send more data + newChatMessageStore.set(null); //This is to prevent a newly created SimplePeer to send an old message a 2nd time. Is there a better way? + }); }); this.on("data", (chunk: Buffer) => { @@ -164,8 +180,9 @@ export class VideoPeer extends Peer { mediaManager.disabledVideoByUserId(this.userId); } } else if (message.type === MESSAGE_TYPE_MESSAGE) { - if (!blackListManager.isBlackListed(message.userId)) { - mediaManager.addNewMessage(message.name, message.message); + if (!blackListManager.isBlackListed(this.userUuid)) { + chatMessagesStore.addExternalMessage(this.userId, message.message); + chatVisibilityStore.set(true); } } else if (message.type === MESSAGE_TYPE_BLOCKED) { //FIXME when A blacklists B, the output stream from A is muted in B's js client. This is insecure since B can manipulate the code to unmute A stream. @@ -253,7 +270,9 @@ export class VideoPeer extends Peer { } this.onBlockSubscribe.unsubscribe(); this.onUnBlockSubscribe.unsubscribe(); - discussionManager.removeParticipant(this.userId); + if (this.newMessageunsubscriber) this.newMessageunsubscriber(); + chatMessagesStore.addOutcomingUser(this.userId); + //discussionManager.removeParticipant(this.userId); // FIXME: I don't understand why "Closing connection with" message is displayed TWICE before "Nb users in peerConnectionArray" // I do understand the method closeConnection is called twice, but I don't understand how they manage to run in parallel. super.destroy(error); diff --git a/front/style/fonts.scss b/front/style/fonts.scss index a49d3967..526f6615 100644 --- a/front/style/fonts.scss +++ b/front/style/fonts.scss @@ -1,9 +1,5 @@ @import "~@fontsource/press-start-2p/index.css"; -*{ - font-family: PixelFont-7,monospace; -} - .nes-btn { font-family: "Press Start 2P"; } diff --git a/front/webpack.config.ts b/front/webpack.config.ts index b6efb389..37362baf 100644 --- a/front/webpack.config.ts +++ b/front/webpack.config.ts @@ -7,7 +7,6 @@ import MiniCssExtractPlugin from "mini-css-extract-plugin"; import sveltePreprocess from "svelte-preprocess"; import ForkTsCheckerWebpackPlugin from "fork-ts-checker-webpack-plugin"; import NodePolyfillPlugin from "node-polyfill-webpack-plugin"; -import { DISPLAY_TERMS_OF_USE } from "./src/Enum/EnvironmentVariable"; const mode = process.env.NODE_ENV ?? "development"; const buildNpmTypingsForApi = !!process.env.BUILD_TYPINGS; From b1cb12861fc10c2b8e2d2e91692bc4139b02deac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Wed, 7 Jul 2021 22:14:59 +0200 Subject: [PATCH 023/101] Migrating variables functions to the "state" namespace. --- CHANGELOG.md | 16 +++- front/src/Api/iframe/room.ts | 54 -------------- front/src/Api/iframe/state.ts | 85 ++++++++++++++++++++++ front/src/Connexion/RoomConnection.ts | 1 - front/src/iframe_api.ts | 4 +- maps/tests/Variables/script.js | 16 ++-- maps/tests/Variables/shared_variables.html | 10 +-- 7 files changed, 117 insertions(+), 69 deletions(-) create mode 100644 front/src/Api/iframe/state.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index a83e8213..e8070634 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,14 +8,24 @@ - Migrated the admin console to Svelte, and redesigned the console #1211 - Layer properties (like `exitUrl`, `silent`, etc...) can now also used in tile properties #1210 (@jonnytest1) - New scripting API features : + - Use `WA.onInit(): Promise` to wait for scripting API initialization - Use `WA.room.showLayer(): void` to show a layer - Use `WA.room.hideLayer(): void` to hide a layer - Use `WA.room.setProperty() : void` to add or change existing property of a layer - Use `WA.player.onPlayerMove(): void` to track the movement of the current player - - Use `WA.room.getCurrentUser(): Promise` to get the ID, name and tags of the current player - - Use `WA.room.getCurrentRoom(): Promise` to get the ID, JSON map file, url of the map of the current room and the layer where the current player started - - Use `WA.ui.registerMenuCommand(): void` to add a custom menu + - Use `WA.player.id: string|undefined` to get the ID of the current player + - Use `WA.player.name: string` to get the name of the current player + - Use `WA.player.tags: string[]` to get the tags of the current player + - Use `WA.room.id: string` to get the ID of the room + - Use `WA.room.mapURL: string` to get the URL of the map + - Use `WA.room.mapURL: string` to get the URL of the map + - Use `WA.room.getMap(): Promise` to get the JSON map file - Use `WA.room.setTiles(): void` to change an array of tiles + - Use `WA.ui.registerMenuCommand(): void` to add a custom menu + - Use `WA.state.loadVariable(key: string): unknown` to retrieve a variable + - Use `WA.state.saveVariable(key: string, value: unknown): Promise` to set a variable (across the room, for all users) + - Use `WA.state.onVariableChange(key: string): Subscription` to track a variable + - Use `WA.state.[any variable]: unknown` to access directly any variable (this is a shortcut to using `WA.state.loadVariable` and `WA.state.saveVariable`) - Users blocking now relies on UUID rather than ID. A blocked user that leaves a room and comes back will stay blocked. ## Version 1.4.3 - 1.4.4 - 1.4.5 diff --git a/front/src/Api/iframe/room.ts b/front/src/Api/iframe/room.ts index db639cd9..9954cb7c 100644 --- a/front/src/Api/iframe/room.ts +++ b/front/src/Api/iframe/room.ts @@ -4,15 +4,11 @@ import { EnterLeaveEvent, isEnterLeaveEvent } from "../Events/EnterLeaveEvent"; import {IframeApiContribution, queryWorkadventure, sendToWorkadventure} from "./IframeApiContribution"; import { apiCallback } from "./registeredCallbacks"; -import {isSetVariableEvent, SetVariableEvent} from "../Events/SetVariableEvent"; import type { ITiledMap } from "../../Phaser/Map/ITiledMap"; const enterStreams: Map> = new Map>(); const leaveStreams: Map> = new Map>(); -const setVariableResolvers = new Subject(); -const variables = new Map(); -const variableSubscribers = new Map>(); interface TileDescriptor { x: number; @@ -33,24 +29,6 @@ export const setMapURL = (url: string) => { mapURL = url; } -export const initVariables = (_variables: Map): void => { - for (const [name, value] of _variables.entries()) { - // In case the user already decided to put values in the variables (before onInit), let's make sure onInit does not override this. - if (!variables.has(name)) { - variables.set(name, value); - } - } - -} - -setVariableResolvers.subscribe((event) => { - variables.set(event.key, event.value); - const subject = variableSubscribers.get(event.key); - if (subject !== undefined) { - subject.next(event.value); - } -}); - export class WorkadventureRoomCommands extends IframeApiContribution { callbacks = [ apiCallback({ @@ -67,13 +45,6 @@ export class WorkadventureRoomCommands extends IframeApiContribution { - setVariableResolvers.next(payloadData); - } - }), ]; onEnterZone(name: string, callback: () => void): void { @@ -119,31 +90,6 @@ export class WorkadventureRoomCommands extends IframeApiContribution { - variables.set(key, value); - return queryWorkadventure({ - type: 'setVariable', - data: { - key, - value - } - }) - } - - loadVariable(key: string): unknown { - return variables.get(key); - } - - onVariableChange(key: string): Observable { - let subject = variableSubscribers.get(key); - if (subject === undefined) { - subject = new Subject(); - variableSubscribers.set(key, subject); - } - return subject.asObservable(); - } - - get id() : string { if (roomId === undefined) { throw new Error('Room id not initialized yet. You should call WA.room.id within a WA.onInit callback.'); diff --git a/front/src/Api/iframe/state.ts b/front/src/Api/iframe/state.ts new file mode 100644 index 00000000..c894e09e --- /dev/null +++ b/front/src/Api/iframe/state.ts @@ -0,0 +1,85 @@ +import {Observable, Subject} from "rxjs"; + +import { EnterLeaveEvent, isEnterLeaveEvent } from "../Events/EnterLeaveEvent"; + +import {IframeApiContribution, queryWorkadventure, sendToWorkadventure} from "./IframeApiContribution"; +import { apiCallback } from "./registeredCallbacks"; +import {isSetVariableEvent, SetVariableEvent} from "../Events/SetVariableEvent"; + +import type { ITiledMap } from "../../Phaser/Map/ITiledMap"; + +const setVariableResolvers = new Subject(); +const variables = new Map(); +const variableSubscribers = new Map>(); + +export const initVariables = (_variables: Map): void => { + for (const [name, value] of _variables.entries()) { + // In case the user already decided to put values in the variables (before onInit), let's make sure onInit does not override this. + if (!variables.has(name)) { + variables.set(name, value); + } + } + +} + +setVariableResolvers.subscribe((event) => { + variables.set(event.key, event.value); + const subject = variableSubscribers.get(event.key); + if (subject !== undefined) { + subject.next(event.value); + } +}); + +export class WorkadventureStateCommands extends IframeApiContribution { + callbacks = [ + apiCallback({ + type: "setVariable", + typeChecker: isSetVariableEvent, + callback: (payloadData) => { + setVariableResolvers.next(payloadData); + } + }), + ]; + + saveVariable(key : string, value : unknown): Promise { + variables.set(key, value); + return queryWorkadventure({ + type: 'setVariable', + data: { + key, + value + } + }) + } + + loadVariable(key: string): unknown { + return variables.get(key); + } + + onVariableChange(key: string): Observable { + let subject = variableSubscribers.get(key); + if (subject === undefined) { + subject = new Subject(); + variableSubscribers.set(key, subject); + } + return subject.asObservable(); + } + +} + +const proxyCommand = new Proxy(new WorkadventureStateCommands(), { + get(target: WorkadventureStateCommands, p: PropertyKey, receiver: unknown): unknown { + if (p in target) { + return Reflect.get(target, p, receiver); + } + return target.loadVariable(p.toString()); + }, + set(target: WorkadventureStateCommands, p: PropertyKey, value: unknown, receiver: unknown): boolean { + // Note: when using "set", there is no way to wait, so we ignore the return of the promise. + // User must use WA.state.saveVariable to have error message. + target.saveVariable(p.toString(), value); + return true; + } +}); + +export default proxyCommand; diff --git a/front/src/Connexion/RoomConnection.ts b/front/src/Connexion/RoomConnection.ts index 53eff010..33122caa 100644 --- a/front/src/Connexion/RoomConnection.ts +++ b/front/src/Connexion/RoomConnection.ts @@ -177,7 +177,6 @@ export class RoomConnection implements RoomConnection { } } } else if (message.hasRoomjoinedmessage()) { - console.error('COUCOU') const roomJoinedMessage = message.getRoomjoinedmessage() as RoomJoinedMessage; const items: { [itemId: number]: unknown } = {}; diff --git a/front/src/iframe_api.ts b/front/src/iframe_api.ts index cd610ab0..2bf1185b 100644 --- a/front/src/iframe_api.ts +++ b/front/src/iframe_api.ts @@ -11,7 +11,8 @@ import nav from "./Api/iframe/nav"; import controls from "./Api/iframe/controls"; import ui from "./Api/iframe/ui"; import sound from "./Api/iframe/sound"; -import room, {initVariables, setMapURL, setRoomId} from "./Api/iframe/room"; +import room, {setMapURL, setRoomId} from "./Api/iframe/room"; +import state, {initVariables} from "./Api/iframe/state"; import player, {setPlayerName, setTags, setUuid} from "./Api/iframe/player"; import type { ButtonDescriptor } from "./Api/iframe/Ui/ButtonDescriptor"; import type { Popup } from "./Api/iframe/Ui/Popup"; @@ -42,6 +43,7 @@ const wa = { sound, room, player, + state, onInit(): Promise { return initPromise; diff --git a/maps/tests/Variables/script.js b/maps/tests/Variables/script.js index 120a4425..ae663cc9 100644 --- a/maps/tests/Variables/script.js +++ b/maps/tests/Variables/script.js @@ -1,20 +1,26 @@ WA.onInit().then(() => { console.log('Trying to read variable "doorOpened" whose default property is true. This should display "true".'); - console.log('doorOpened', WA.room.loadVariable('doorOpened')); + console.log('doorOpened', WA.state.loadVariable('doorOpened')); console.log('Trying to set variable "not_exists". This should display an error in the console, followed by a log saying the error was caught.') - WA.room.saveVariable('not_exists', 'foo').catch((e) => { + WA.state.saveVariable('not_exists', 'foo').catch((e) => { console.log('Successfully caught error: ', e); }); console.log('Trying to set variable "myvar". This should work.'); - WA.room.saveVariable('myvar', {'foo': 'bar'}); + WA.state.saveVariable('myvar', {'foo': 'bar'}); console.log('Trying to read variable "myvar". This should display a {"foo": "bar"} object.'); - console.log(WA.room.loadVariable('myvar')); + console.log(WA.state.loadVariable('myvar')); + + console.log('Trying to set variable "myvar" using proxy. This should work.'); + WA.state.myvar = {'baz': 42}; + + console.log('Trying to read variable "myvar" using proxy. This should display a {"baz": 42} object.'); + console.log(WA.state.myvar); console.log('Trying to set variable "config". This should not work because we are not logged as admin.'); - WA.room.saveVariable('config', {'foo': 'bar'}).catch(e => { + WA.state.saveVariable('config', {'foo': 'bar'}).catch(e => { console.log('Successfully caught error because variable "config" is not writable: ', e); }); }); diff --git a/maps/tests/Variables/shared_variables.html b/maps/tests/Variables/shared_variables.html index ae282b1c..80fdbdd4 100644 --- a/maps/tests/Variables/shared_variables.html +++ b/maps/tests/Variables/shared_variables.html @@ -12,21 +12,21 @@ WA.onInit().then(() => { console.log('After WA init'); const textField = document.getElementById('textField'); - textField.value = WA.room.loadVariable('textField'); + textField.value = WA.state.loadVariable('textField'); textField.addEventListener('change', function (evt) { console.log('saving variable') - WA.room.saveVariable('textField', this.value); + WA.state.saveVariable('textField', this.value); }); - WA.room.onVariableChange('textField').subscribe((value) => { + WA.state.onVariableChange('textField').subscribe((value) => { console.log('variable changed received') textField.value = value; }); document.getElementById('btn').addEventListener('click', () => { - console.log(WA.room.loadVariable('textField')); - document.getElementById('placeholder').innerText = WA.room.loadVariable('textField'); + console.log(WA.state.loadVariable('textField')); + document.getElementById('placeholder').innerText = WA.state.loadVariable('textField'); }); }); }) From 52fd9067b80ddc6054a2e58f368552a1475c70ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Thu, 8 Jul 2021 11:46:30 +0200 Subject: [PATCH 024/101] Editing do to add "state" API doc --- CHANGELOG.md | 2 +- docs/maps/api-reference.md | 1 + docs/maps/api-room.md | 77 ------------------------- docs/maps/api-state.md | 105 ++++++++++++++++++++++++++++++++++ front/src/Api/iframe/state.ts | 7 +++ 5 files changed, 114 insertions(+), 78 deletions(-) create mode 100644 docs/maps/api-state.md diff --git a/CHANGELOG.md b/CHANGELOG.md index e8070634..33658d2d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,7 +24,7 @@ - Use `WA.ui.registerMenuCommand(): void` to add a custom menu - Use `WA.state.loadVariable(key: string): unknown` to retrieve a variable - Use `WA.state.saveVariable(key: string, value: unknown): Promise` to set a variable (across the room, for all users) - - Use `WA.state.onVariableChange(key: string): Subscription` to track a variable + - Use `WA.state.onVariableChange(key: string): Observable` to track a variable - Use `WA.state.[any variable]: unknown` to access directly any variable (this is a shortcut to using `WA.state.loadVariable` and `WA.state.saveVariable`) - Users blocking now relies on UUID rather than ID. A blocked user that leaves a room and comes back will stay blocked. diff --git a/docs/maps/api-reference.md b/docs/maps/api-reference.md index 2fcc4613..d044668f 100644 --- a/docs/maps/api-reference.md +++ b/docs/maps/api-reference.md @@ -5,6 +5,7 @@ - [Navigation functions](api-nav.md) - [Chat functions](api-chat.md) - [Room functions](api-room.md) +- [State related functions](api-state.md) - [Player functions](api-player.md) - [UI functions](api-ui.md) - [Sound functions](api-sound.md) diff --git a/docs/maps/api-room.md b/docs/maps/api-room.md index ad79f246..9f911b35 100644 --- a/docs/maps/api-room.md +++ b/docs/maps/api-room.md @@ -158,80 +158,3 @@ WA.room.setTiles([ {x: 9, y: 4, tile: 'blue', layer: 'setTiles'} ]); ``` - -### Saving / loading state - -``` -WA.room.saveVariable(key : string, data : unknown): void -WA.room.loadVariable(key : string) : unknown -WA.room.onVariableChange(key : string).subscribe((data: unknown) => {}) : Subscription -``` - -These 3 methods can be used to save, load and track changes in variables related to the current room. - -`data` can be any value that is serializable in JSON. - -Please refrain from storing large amounts of data in a room. Those functions are typically useful for saving or restoring -configuration / metadatas. - -Example : -```javascript -WA.room.saveVariable('config', { - 'bottomExitUrl': '/@/org/world/castle', - 'topExitUrl': '/@/org/world/tower', - 'enableBirdSound': true -}); -//... -let config = WA.room.loadVariable('config'); -``` - -If you are using Typescript, please note that the return type of `loadVariable` is `unknown`. This is -for security purpose, as we don't know the type of the variable. In order to use the returned value, -you will need to cast it to the correct type (or better, use a [Type guard](https://www.typescriptlang.org/docs/handbook/2/narrowing.html) to actually check at runtime -that you get the expected type). - -{.alert.alert-warning} -For security reasons, you cannot load or save **any** variable (otherwise, anyone on your map could set any data). -Variables storage is subject to an authorization process. Read below to learn more. - -#### Declaring allowed keys - -In order to declare allowed keys related to a room, you need to add a **objects** in an "object layer" of the map. - -Each object will represent a variable. - -
-
- -
-
- -TODO: move the image in https://workadventu.re/img/docs - - -The name of the variable is the name of the object. -The object **type** MUST be **variable**. - -You can set a default value for the object in the `default` property. - -Use the `persist` property to save the state of the variable in database. If `persist` is false, the variable will stay -in the memory of the WorkAdventure servers but will be wiped out of the memory as soon as the room is empty (or if the -server restarts). - -{.alert.alert-info} -Do not use `persist` for highly dynamic values that have a short life spawn. - -With `readableBy` and `writableBy`, you control who can read of write in this variable. The property accepts a string -representing a "tag". Anyone having this "tag" can read/write in the variable. - -{.alert.alert-warning} -`readableBy` and `writableBy` are specific to the public version of WorkAdventure because the notion of tags -is not available unless you have an "admin" server (that is not part of the self-hosted version of WorkAdventure). - -Finally, the `jsonSchema` property can contain [a complete JSON schema](https://json-schema.org/) to validate the content of the variable. -Trying to set a variable to a value that is not compatible with the schema will fail. - - - - -TODO: document tracking, unsubscriber, etc... diff --git a/docs/maps/api-state.md b/docs/maps/api-state.md new file mode 100644 index 00000000..6b74389b --- /dev/null +++ b/docs/maps/api-state.md @@ -0,0 +1,105 @@ +{.section-title.accent.text-primary} +# API state related functions Reference + +### Saving / loading state + +The `WA.state` functions allow you to easily share a common state between all the players in a given room. +Moreover, `WA.state` functions can be used to persist this state across reloads. + +``` +WA.state.saveVariable(key : string, data : unknown): void +WA.state.loadVariable(key : string) : unknown +WA.state.onVariableChange(key : string).subscribe((data: unknown) => {}) : Subscription +WA.state.[any property]: unknown +``` + +These methods and properties can be used to save, load and track changes in variables related to the current room. + +Variables stored in `WA.state` can be any value that is serializable in JSON. + +Please refrain from storing large amounts of data in a room. Those functions are typically useful for saving or restoring +configuration / metadata. + +{.alert.alert-warning} +We are in the process of fine-tuning variables, and we will eventually put limits on the maximum size a variable can hold. We will also put limits on the number of calls you can make to saving variables, so don't change the value of a variable every 10ms, this will fail in the future. + + +Example : +```javascript +WA.state.saveVariable('config', { + 'bottomExitUrl': '/@/org/world/castle', + 'topExitUrl': '/@/org/world/tower', + 'enableBirdSound': true +}).catch(e => console.error('Something went wrong while saving variable', e)); +//... +let config = WA.state.loadVariable('config'); +``` + +You can use the shortcut properties to load and save variables. The code above is similar to: + +```javascript +WA.state.config = { + 'bottomExitUrl': '/@/org/world/castle', + 'topExitUrl': '/@/org/world/tower', + 'enableBirdSound': true +}; + +//... +let config = WA.state.config; +``` + +Note: `saveVariable` returns a promise that will fail in case the variable cannot be saved. This +can happen if your user does not have the required rights (more on that in the next chapter). +In contrast, if you use the WA.state properties, you cannot access the promise and therefore cannot +know for sure if your variable was properly saved. + +If you are using Typescript, please note that the type of variables is `unknown`. This is +for security purpose, as we don't know the type of the variable. In order to use the returned value, +you will need to cast it to the correct type (or better, use a [Type guard](https://www.typescriptlang.org/docs/handbook/2/narrowing.html) to actually check at runtime +that you get the expected type). + +{.alert.alert-warning} +For security reasons, the list of variables you are allowed to access and modify is **restricted** (otherwise, anyone on your map could set any data). +Variables storage is subject to an authorization process. Read below to learn more. + +#### Declaring allowed keys + +In order to declare allowed keys related to a room, you need to add **objects** in an "object layer" of the map. + +Each object will represent a variable. + +
+
+ +
+
+ +TODO: move the image in https://workadventu.re/img/docs + + +The name of the variable is the name of the object. +The object **type** MUST be **variable**. + +You can set a default value for the object in the `default` property. + +Use the `persist` property to save the state of the variable in database. If `persist` is false, the variable will stay +in the memory of the WorkAdventure servers but will be wiped out of the memory as soon as the room is empty (or if the +server restarts). + +{.alert.alert-info} +Do not use `persist` for highly dynamic values that have a short life spawn. + +With `readableBy` and `writableBy`, you control who can read of write in this variable. The property accepts a string +representing a "tag". Anyone having this "tag" can read/write in the variable. + +{.alert.alert-warning} +`readableBy` and `writableBy` are specific to the public version of WorkAdventure because the notion of tags +is not available unless you have an "admin" server (that is not part of the self-hosted version of WorkAdventure). + +Finally, the `jsonSchema` property can contain [a complete JSON schema](https://json-schema.org/) to validate the content of the variable. +Trying to set a variable to a value that is not compatible with the schema will fail. + + + + +TODO: document tracking, unsubscriber, etc... diff --git a/front/src/Api/iframe/state.ts b/front/src/Api/iframe/state.ts index c894e09e..90e8cb81 100644 --- a/front/src/Api/iframe/state.ts +++ b/front/src/Api/iframe/state.ts @@ -23,6 +23,13 @@ export const initVariables = (_variables: Map): void => { } setVariableResolvers.subscribe((event) => { + const oldValue = variables.get(event.key); + + // If we are setting the same value, no need to do anything. + if (oldValue === event.value) { + return; + } + variables.set(event.key, event.value); const subject = variableSubscribers.get(event.key); if (subject !== undefined) { From b9a2433283766e2ff61a1f753d956a41abf37b86 Mon Sep 17 00:00:00 2001 From: GRL Date: Mon, 12 Jul 2021 11:59:05 +0200 Subject: [PATCH 025/101] Upgrade graphic of the chat --- front/src/Components/Chat/Chat.svelte | 12 ++++++++++-- front/src/Components/Chat/ChatElement.svelte | 4 ++-- front/src/Components/Chat/ChatMessageForm.svelte | 2 ++ front/src/WebRtc/ColorGenerator.ts | 6 +++++- 4 files changed, 19 insertions(+), 5 deletions(-) diff --git a/front/src/Components/Chat/Chat.svelte b/front/src/Components/Chat/Chat.svelte index c4f9075d..093d01a5 100644 --- a/front/src/Components/Chat/Chat.svelte +++ b/front/src/Components/Chat/Chat.svelte @@ -32,7 +32,7 @@