diff --git a/back/package.json b/back/package.json index 92fa8410..fffd973a 100644 --- a/back/package.json +++ b/back/package.json @@ -40,7 +40,7 @@ }, "homepage": "https://github.com/thecodingmachine/workadventure#readme", "dependencies": { - "@workadventure/tiled-map-type-guard": "^1.0.0", + "@workadventure/tiled-map-type-guard": "^1.0.2", "axios": "^0.21.1", "busboy": "^0.3.1", "circular-json": "^0.5.9", diff --git a/back/src/Services/VariablesManager.ts b/back/src/Services/VariablesManager.ts index e8aaef25..915c6c05 100644 --- a/back/src/Services/VariablesManager.ts +++ b/back/src/Services/VariablesManager.ts @@ -1,7 +1,12 @@ /** * Handles variables shared between the scripting API and the server. */ -import { ITiledMap, ITiledMapObject, ITiledMapObjectLayer } from "@workadventure/tiled-map-type-guard/dist"; +import { + ITiledMap, + ITiledMapLayer, + ITiledMapObject, + ITiledMapObjectLayer, +} from "@workadventure/tiled-map-type-guard/dist"; import { User } from "_Model/User"; import { variablesRepository } from "./Repository/VariablesRepository"; import { redisClient } from "./RedisClient"; @@ -83,25 +88,33 @@ export class VariablesManager { private static findVariablesInMap(map: ITiledMap): Map { const objects = new Map(); for (const layer of map.layers) { - if (layer.type === "objectgroup") { - for (const object of (layer as ITiledMapObjectLayer).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.' - ); - continue; - } - - // We store a copy of the object (to make it immutable) - objects.set(object.name, this.iTiledObjectToVariable(object)); - } - } - } + this.recursiveFindVariablesInLayer(layer, objects); } return objects; } + private static recursiveFindVariablesInLayer(layer: ITiledMapLayer, objects: Map): void { + 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.' + ); + continue; + } + + // We store a copy of the object (to make it immutable) + objects.set(object.name, this.iTiledObjectToVariable(object)); + } + } + } else if (layer.type === "group") { + for (const innerLayer of layer.layers) { + this.recursiveFindVariablesInLayer(innerLayer, objects); + } + } + } + private static iTiledObjectToVariable(object: ITiledMapObject): Variable { const variable: Variable = {}; diff --git a/back/yarn.lock b/back/yarn.lock index e9e07617..228624ab 100644 --- a/back/yarn.lock +++ b/back/yarn.lock @@ -194,10 +194,10 @@ semver "^7.3.2" tsutils "^3.17.1" -"@workadventure/tiled-map-type-guard@^1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@workadventure/tiled-map-type-guard/-/tiled-map-type-guard-1.0.0.tgz#02524602ee8b2688429a1f56df1d04da3fc171ba" - integrity sha512-Mc0SE128otQnYlScQWVaQVyu1+CkailU/FTBh09UTrVnBAhyMO+jIn9vT9+Dv244xq+uzgQDpXmiVdjgrYFQ+A== +"@workadventure/tiled-map-type-guard@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@workadventure/tiled-map-type-guard/-/tiled-map-type-guard-1.0.2.tgz#4171550f6cd71be19791faef48360d65d698bcb0" + integrity sha512-RCtygGV5y9cb7QoyGMINBE9arM5pyXjkxvXgA5uXEv4GDbXKorhFim/rHgwbVR+eFnVF3rDgWbRnk3DIaHt+lQ== dependencies: generic-type-guard "^3.4.1" diff --git a/front/src/Phaser/Game/GameMapPropertiesListener.ts b/front/src/Phaser/Game/GameMapPropertiesListener.ts new file mode 100644 index 00000000..db100935 --- /dev/null +++ b/front/src/Phaser/Game/GameMapPropertiesListener.ts @@ -0,0 +1,55 @@ +import type { GameScene } from "./GameScene"; +import type { GameMap } from "./GameMap"; +import { scriptUtils } from "../../Api/ScriptUtils"; +import { coWebsiteManager } from "../../WebRtc/CoWebsiteManager"; +import { layoutManagerActionStore } from "../../Stores/LayoutManagerStore"; +import { + ON_ACTION_TRIGGER_BUTTON, + TRIGGER_WEBSITE_PROPERTIES, + WEBSITE_MESSAGE_PROPERTIES, +} from "../../WebRtc/LayoutManager"; + +export class GameMapPropertiesListener { + constructor(private scene: GameScene, private gameMap: GameMap) {} + + register() { + this.gameMap.onPropertyChange("openTab", (newValue) => { + if (typeof newValue == "string" && newValue.length) { + scriptUtils.openTab(newValue); + } + }); + this.gameMap.onPropertyChange("openWebsite", (newValue, oldValue, allProps) => { + if (newValue === undefined) { + layoutManagerActionStore.removeAction("openWebsite"); + coWebsiteManager.closeCoWebsite(); + } else { + const openWebsiteFunction = () => { + coWebsiteManager.loadCoWebsite( + newValue as string, + this.scene.MapUrlFile, + allProps.get("openWebsiteAllowApi") as boolean | undefined, + allProps.get("openWebsitePolicy") as string | undefined, + allProps.get("openWebsiteWidth") as number | undefined + ); + layoutManagerActionStore.removeAction("openWebsite"); + }; + const openWebsiteTriggerValue = allProps.get(TRIGGER_WEBSITE_PROPERTIES); + if (openWebsiteTriggerValue && openWebsiteTriggerValue === ON_ACTION_TRIGGER_BUTTON) { + let message = allProps.get(WEBSITE_MESSAGE_PROPERTIES); + if (message === undefined) { + message = "Press SPACE or touch here to open web site"; + } + layoutManagerActionStore.addAction({ + uuid: "openWebsite", + type: "message", + message: message, + callback: () => openWebsiteFunction(), + userInputManager: this.scene.userInputManager, + }); + } else { + openWebsiteFunction(); + } + } + }); + } +} diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 69470fca..aa9debfb 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -92,6 +92,7 @@ import Tileset = Phaser.Tilemaps.Tileset; import { userIsAdminStore } from "../../Stores/GameStore"; import { layoutManagerActionStore } from "../../Stores/LayoutManagerStore"; import { EmbeddedWebsiteManager } from "./EmbeddedWebsiteManager"; +import { GameMapPropertiesListener } from "./GameMapPropertiesListener"; export interface GameSceneInitInterface { initPosition: PointInterface | null; @@ -580,6 +581,7 @@ export class GameScene extends DirtyScene { this.updateCameraOffset(box) ); + new GameMapPropertiesListener(this, this.gameMap).register(); this.triggerOnMapLayerPropertyChange(); if (!this.room.isDisconnected()) { @@ -825,40 +827,7 @@ export class GameScene extends DirtyScene { }, 2000); } }); - this.gameMap.onPropertyChange("openWebsite", (newValue, oldValue, allProps) => { - if (newValue === undefined) { - layoutManagerActionStore.removeAction("openWebsite"); - coWebsiteManager.closeCoWebsite(); - } else { - const openWebsiteFunction = () => { - coWebsiteManager.loadCoWebsite( - newValue as string, - this.MapUrlFile, - allProps.get("openWebsiteAllowApi") as boolean | undefined, - allProps.get("openWebsitePolicy") as string | undefined, - allProps.get("openWebsiteWidth") as number | undefined - ); - layoutManagerActionStore.removeAction("openWebsite"); - }; - const openWebsiteTriggerValue = allProps.get(TRIGGER_WEBSITE_PROPERTIES); - if (openWebsiteTriggerValue && openWebsiteTriggerValue === ON_ACTION_TRIGGER_BUTTON) { - let message = allProps.get(WEBSITE_MESSAGE_PROPERTIES); - if (message === undefined) { - message = "Press SPACE or touch here to open web site"; - } - layoutManagerActionStore.addAction({ - uuid: "openWebsite", - type: "message", - message: message, - callback: () => openWebsiteFunction(), - userInputManager: this.userInputManager, - }); - } else { - openWebsiteFunction(); - } - } - }); this.gameMap.onPropertyChange("jitsiRoom", (newValue, oldValue, allProps) => { if (newValue === undefined) { layoutManagerActionStore.removeAction("jitsi"); diff --git a/front/src/Phaser/Game/SharedVariablesManager.ts b/front/src/Phaser/Game/SharedVariablesManager.ts index 6a06d97e..76b78d04 100644 --- a/front/src/Phaser/Game/SharedVariablesManager.ts +++ b/front/src/Phaser/Game/SharedVariablesManager.ts @@ -1,10 +1,7 @@ import type { RoomConnection } from "../../Connexion/RoomConnection"; import { iframeListener } from "../../Api/IframeListener"; -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"; +import type {ITiledMapLayer, ITiledMapObject, ITiledMapObjectLayer} from "../Map/ITiledMap"; interface Variable { defaultValue: unknown; @@ -100,24 +97,33 @@ export class SharedVariablesManager { 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") { - 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)); - } - } - } + this.recursiveFindVariablesInLayer(layer, objects); } return objects; } + private static recursiveFindVariablesInLayer(layer: ITiledMapLayer, objects: Map): void { + 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.' + ); + continue; + } + + // We store a copy of the object (to make it immutable) + objects.set(object.name, this.iTiledObjectToVariable(object)); + } + } + } else if (layer.type === "group") { + for (const innerLayer of layer.layers) { + this.recursiveFindVariablesInLayer(innerLayer, objects); + } + } + } + private static iTiledObjectToVariable(object: ITiledMapObject): Variable { const variable: Variable = { defaultValue: undefined, diff --git a/front/src/WebRtc/CoWebsiteManager.ts b/front/src/WebRtc/CoWebsiteManager.ts index 53b253e3..d421daf1 100644 --- a/front/src/WebRtc/CoWebsiteManager.ts +++ b/front/src/WebRtc/CoWebsiteManager.ts @@ -168,7 +168,13 @@ class CoWebsiteManager { return iframe; } - public loadCoWebsite(url: string, base: string, allowApi?: boolean, allowPolicy?: string, widthPercent?: number): void { + public loadCoWebsite( + url: string, + base: string, + allowApi?: boolean, + allowPolicy?: string, + widthPercent?: number + ): void { this.load(); this.cowebsiteMainDom.innerHTML = ``; diff --git a/front/src/WebRtc/JitsiFactory.ts b/front/src/WebRtc/JitsiFactory.ts index 1cc67273..8e3a37c7 100644 --- a/front/src/WebRtc/JitsiFactory.ts +++ b/front/src/WebRtc/JitsiFactory.ts @@ -1,37 +1,67 @@ -import {JITSI_URL} from "../Enum/EnvironmentVariable"; -import {mediaManager} from "./MediaManager"; -import {coWebsiteManager} from "./CoWebsiteManager"; -import {requestedCameraState, requestedMicrophoneState} from "../Stores/MediaStore"; -import {get} from "svelte/store"; -declare const window:any; // eslint-disable-line @typescript-eslint/no-explicit-any +import { JITSI_URL } from "../Enum/EnvironmentVariable"; +import { coWebsiteManager } from "./CoWebsiteManager"; +import { requestedCameraState, requestedMicrophoneState } from "../Stores/MediaStore"; +import { get } from "svelte/store"; interface jitsiConfigInterface { - startWithAudioMuted: boolean - startWithVideoMuted: boolean - prejoinPageEnabled: boolean + startWithAudioMuted: boolean; + startWithVideoMuted: boolean; + prejoinPageEnabled: boolean; } -const getDefaultConfig = () : jitsiConfigInterface => { - return { - startWithAudioMuted: !get(requestedMicrophoneState), - startWithVideoMuted: !get(requestedCameraState), - prejoinPageEnabled: false +interface JitsiOptions { + jwt?: string; + roomName: string; + width: string; + height: string; + parentNode: HTMLElement; + configOverwrite: jitsiConfigInterface; + interfaceConfigOverwrite: typeof defaultInterfaceConfig; + onload?: Function; +} + +interface JitsiApi { + executeCommand: (command: string, ...args: Array) => void; + + addListener: (type: string, callback: Function) => void; + removeListener: (type: string, callback: Function) => void; + + dispose: () => void; +} + +declare global { + interface Window { + JitsiMeetExternalAPI: new (domain: string, options: JitsiOptions) => JitsiApi; } } +const getDefaultConfig = (): jitsiConfigInterface => { + return { + startWithAudioMuted: !get(requestedMicrophoneState), + startWithVideoMuted: !get(requestedCameraState), + prejoinPageEnabled: false, + }; +}; + const mergeConfig = (config?: object) => { const currentDefaultConfig = getDefaultConfig(); - if(!config){ + if (!config) { return currentDefaultConfig; } return { ...currentDefaultConfig, ...config, - startWithAudioMuted: (config as jitsiConfigInterface).startWithAudioMuted ? true : currentDefaultConfig.startWithAudioMuted, - startWithVideoMuted: (config as jitsiConfigInterface).startWithVideoMuted ? true : currentDefaultConfig.startWithVideoMuted, - prejoinPageEnabled: (config as jitsiConfigInterface).prejoinPageEnabled ? true : currentDefaultConfig.prejoinPageEnabled - } -} + startWithAudioMuted: (config as jitsiConfigInterface).startWithAudioMuted + ? true + : currentDefaultConfig.startWithAudioMuted, + startWithVideoMuted: (config as jitsiConfigInterface).startWithVideoMuted + ? true + : currentDefaultConfig.startWithVideoMuted, + prejoinPageEnabled: (config as jitsiConfigInterface).prejoinPageEnabled + ? true + : currentDefaultConfig.prejoinPageEnabled, + }; +}; const defaultInterfaceConfig = { SHOW_CHROME_EXTENSION_BANNER: false, @@ -49,28 +79,48 @@ const defaultInterfaceConfig = { SHOW_WATERMARK_FOR_GUESTS: false, TOOLBAR_BUTTONS: [ - 'microphone', 'camera', 'closedcaptions', 'desktop', /*'embedmeeting',*/ 'fullscreen', - 'fodeviceselection', 'hangup', 'profile', 'chat', 'recording', - 'livestreaming', 'etherpad', 'sharedvideo', 'settings', 'raisehand', - 'videoquality', 'filmstrip', /*'invite',*/ 'feedback', 'stats', 'shortcuts', - 'tileview', 'videobackgroundblur', 'download', 'help', 'mute-everyone', /*'security'*/ + "microphone", + "camera", + "closedcaptions", + "desktop", + /*'embedmeeting',*/ "fullscreen", + "fodeviceselection", + "hangup", + "profile", + "chat", + "recording", + "livestreaming", + "etherpad", + "sharedvideo", + "settings", + "raisehand", + "videoquality", + "filmstrip", + /*'invite',*/ "feedback", + "stats", + "shortcuts", + "tileview", + "videobackgroundblur", + "download", + "help", + "mute-everyone" /*'security'*/, ], }; const slugify = (...args: (string | number)[]): string => { - const value = args.join(' ') + const value = args.join(" "); return value - .normalize('NFD') // split an accented letter in the base letter and the accent - .replace(/[\u0300-\u036f]/g, '') // remove all previously split accents + .normalize("NFD") // split an accented letter in the base letter and the accent + .replace(/[\u0300-\u036f]/g, "") // remove all previously split accents .toLowerCase() .trim() - .replace(/[^a-z0-9 ]/g, '') // remove all chars not letters, numbers and spaces (to be replaced) - .replace(/\s+/g, '-') // separator -} + .replace(/[^a-z0-9 ]/g, "") // remove all chars not letters, numbers and spaces (to be replaced) + .replace(/\s+/g, "-"); // separator +}; class JitsiFactory { - private jitsiApi: any; // eslint-disable-line @typescript-eslint/no-explicit-any + private jitsiApi?: JitsiApi; private audioCallback = this.onAudioChange.bind(this); private videoCallback = this.onVideoChange.bind(this); private jitsiScriptLoaded: boolean = false; @@ -79,11 +129,19 @@ class JitsiFactory { * Slugifies the room name and prepends the room name with the instance */ public getRoomName(roomName: string, instance: string): string { - return slugify(instance.replace('/', '-') + "-" + roomName); + return slugify(instance.replace("/", "-") + "-" + roomName); } - public start(roomName: string, playerName:string, jwt?: string, config?: object, interfaceConfig?: object, jitsiUrl?: string, jitsiWidth?: number): void { - coWebsiteManager.insertCoWebsite((async cowebsiteDiv => { + public start( + roomName: string, + playerName: string, + jwt?: string, + config?: object, + interfaceConfig?: object, + jitsiUrl?: string, + jitsiWidth?: number + ): void { + coWebsiteManager.insertCoWebsite(async (cowebsiteDiv) => { // Jitsi meet external API maintains some data in local storage // which is sent via the appData URL parameter when joining a // conference. Problem is that this data grows indefinitely. Thus @@ -94,18 +152,18 @@ class JitsiFactory { const domain = jitsiUrl || JITSI_URL; if (domain === undefined) { - throw new Error('Missing JITSI_URL environment variable or jitsiUrl parameter in the map.') + throw new Error("Missing JITSI_URL environment variable or jitsiUrl parameter in the map."); } await this.loadJitsiScript(domain); - const options: any = { // eslint-disable-line @typescript-eslint/no-explicit-any + const options: JitsiOptions = { roomName: roomName, jwt: jwt, width: "100%", height: "100%", parentNode: cowebsiteDiv, configOverwrite: mergeConfig(config), - interfaceConfigOverwrite: {...defaultInterfaceConfig, ...interfaceConfig} + interfaceConfigOverwrite: { ...defaultInterfaceConfig, ...interfaceConfig }, }; if (!options.jwt) { delete options.jwt; @@ -115,25 +173,25 @@ class JitsiFactory { options.onload = () => resolve(); //we want for the iframe to be loaded before triggering animations. setTimeout(() => resolve(), 2000); //failsafe in case the iframe is deleted before loading or too long to load this.jitsiApi = new window.JitsiMeetExternalAPI(domain, options); - this.jitsiApi.executeCommand('displayName', playerName); + this.jitsiApi.executeCommand("displayName", playerName); - this.jitsiApi.addListener('audioMuteStatusChanged', this.audioCallback); - this.jitsiApi.addListener('videoMuteStatusChanged', this.videoCallback); + this.jitsiApi.addListener("audioMuteStatusChanged", this.audioCallback); + this.jitsiApi.addListener("videoMuteStatusChanged", this.videoCallback); }); - }), jitsiWidth); + }, jitsiWidth); } public async stop(): Promise { - if(!this.jitsiApi){ + if (!this.jitsiApi) { return; } await coWebsiteManager.closeCoWebsite(); - this.jitsiApi.removeListener('audioMuteStatusChanged', this.audioCallback); - this.jitsiApi.removeListener('videoMuteStatusChanged', this.videoCallback); + this.jitsiApi.removeListener("audioMuteStatusChanged", this.audioCallback); + this.jitsiApi.removeListener("videoMuteStatusChanged", this.videoCallback); this.jitsiApi?.dispose(); } - private onAudioChange({muted}: {muted: boolean}): void { + private onAudioChange({ muted }: { muted: boolean }): void { if (muted) { requestedMicrophoneState.disableMicrophone(); } else { @@ -141,7 +199,7 @@ class JitsiFactory { } } - private onVideoChange({muted}: {muted: boolean}): void { + private onVideoChange({ muted }: { muted: boolean }): void { if (muted) { requestedCameraState.disableWebcam(); } else { @@ -159,20 +217,17 @@ class JitsiFactory { this.jitsiScriptLoaded = true; // Load Jitsi if the environment variable is set. - const jitsiScript = document.createElement('script'); - jitsiScript.src = 'https://' + domain + '/external_api.js'; + const jitsiScript = document.createElement("script"); + jitsiScript.src = "https://" + domain + "/external_api.js"; jitsiScript.onload = () => { resolve(); - } + }; jitsiScript.onerror = () => { reject(); - } + }; document.head.appendChild(jitsiScript); - - }) - - + }); } }