From 5472d220ba10e0bb6245e65c526d3ebe947a171b Mon Sep 17 00:00:00 2001 From: jonny Date: Wed, 23 Jun 2021 17:32:32 +0200 Subject: [PATCH 001/106] added trigger message code --- docs/maps/api-ui.md | 22 ++++++- front/src/Api/Events/IframeEvent.ts | 5 ++ .../src/Api/Events/ui/TriggerMessageEvent.ts | 21 ++++++ .../Events/ui/TriggerMessageEventHandler.ts | 42 ++++++++++++ front/src/Api/IframeListener.ts | 53 ++++++++------- front/src/Api/iframe/Ui/TriggerMessage.ts | 51 ++++++++++++++ front/src/Api/iframe/ui.ts | 66 ++++++++++--------- front/src/Phaser/Game/GameScene.ts | 46 ++++++++----- maps/tests/script.js | 60 +++++++++-------- 9 files changed, 267 insertions(+), 99 deletions(-) create mode 100644 front/src/Api/Events/ui/TriggerMessageEvent.ts create mode 100644 front/src/Api/Events/ui/TriggerMessageEventHandler.ts create mode 100644 front/src/Api/iframe/Ui/TriggerMessage.ts diff --git a/docs/maps/api-ui.md b/docs/maps/api-ui.md index 286f2ac7..b1d244da 100644 --- a/docs/maps/api-ui.md +++ b/docs/maps/api-ui.md @@ -86,4 +86,24 @@ WA.ui.registerMenuCommand("test", () => {
-
\ No newline at end of file + + + + +### Awaiting User Confirmation (with space bar) + +```typescript +triggerMessage(message: string): TriggerMessage +``` + +Displays a message at the bottom of the screen (that will disappear when space bar is pressed). + +Example: + +```javascript +const triggerMessage = WA.ui.triggerMessage("press 'space' to confirm"); +setTimeout(()=>{ + // later + triggerMessage.remove(); +},1000) +``` \ No newline at end of file diff --git a/front/src/Api/Events/IframeEvent.ts b/front/src/Api/Events/IframeEvent.ts index 7325f811..d2df4ded 100644 --- a/front/src/Api/Events/IframeEvent.ts +++ b/front/src/Api/Events/IframeEvent.ts @@ -18,6 +18,7 @@ import type { PlaySoundEvent } from "./PlaySoundEvent"; import type { MenuItemClickedEvent } from "./ui/MenuItemClickedEvent"; import type { MenuItemRegisterEvent } from './ui/MenuItemRegisterEvent'; import type { HasPlayerMovedEvent } from "./HasPlayerMovedEvent"; +import type { MessageReferenceEvent, TriggerMessageEvent } from '../iframe/TriggerMessageEvent'; export interface TypedMessageEvent extends MessageEvent { @@ -49,6 +50,9 @@ export type IframeEventMap = { stopSound: null, getState: undefined, registerMenuCommand: MenuItemRegisterEvent + + triggerMessage: TriggerMessageEvent + removeTriggerMessage: MessageReferenceEvent } export interface IframeEvent { type: T; @@ -68,6 +72,7 @@ export interface IframeResponseEventMap { hasPlayerMoved: HasPlayerMovedEvent dataLayer: DataLayerEvent menuItemClicked: MenuItemClickedEvent + messageTriggered: MessageReferenceEvent } export interface IframeResponseEvent { type: T; diff --git a/front/src/Api/Events/ui/TriggerMessageEvent.ts b/front/src/Api/Events/ui/TriggerMessageEvent.ts new file mode 100644 index 00000000..5b42b02e --- /dev/null +++ b/front/src/Api/Events/ui/TriggerMessageEvent.ts @@ -0,0 +1,21 @@ +import * as tg from "generic-type-guard"; + +export const triggerMessage = "triggerMessage" +export const removeTriggerMessage = "removeTriggerMessage" + +export const isTriggerMessageEvent = new tg.IsInterface().withProperties({ + message: tg.isString, + uuid: tg.isString +}).get() + + +export type TriggerMessageEvent = tg.GuardedType; + + +export const isMessageReferenceEvent = + new tg.IsInterface().withProperties({ + uuid: tg.isString + }).get(); + + +export type MessageReferenceEvent = tg.GuardedType; diff --git a/front/src/Api/Events/ui/TriggerMessageEventHandler.ts b/front/src/Api/Events/ui/TriggerMessageEventHandler.ts new file mode 100644 index 00000000..d690dbc0 --- /dev/null +++ b/front/src/Api/Events/ui/TriggerMessageEventHandler.ts @@ -0,0 +1,42 @@ +import { Subject } from 'rxjs'; +import { iframeListener } from '../../IframeListener'; +import { isMessageReferenceEvent, isTriggerMessageEvent, MessageReferenceEvent, removeTriggerMessage, triggerMessage, TriggerMessageEvent } from './TriggerMessageEvent'; +import * as tg from "generic-type-guard"; +export function sendMessageTriggeredEvent(uuid: string) { + iframeListener.postMessage({ + 'type': 'messageTriggered', + 'data': { + uuid, + } as MessageReferenceEvent + }); +} + +const _triggerMessageEvent: Subject = new Subject(); +const _removeTriggerMessageEvent: Subject = new Subject(); + +export const triggerMessageEvent = _triggerMessageEvent.asObservable(); + +export const removeTriggerMessageEvent = _removeTriggerMessageEvent.asObservable(); + +const isTriggerMessageEventObject = new tg.IsInterface().withProperties({ + type: tg.isSingletonString(triggerMessage), + data: isTriggerMessageEvent +}).get() +const isTriggerMessageRemoveEventObject = new tg.IsInterface().withProperties({ + type: tg.isSingletonString(removeTriggerMessage), + data: isMessageReferenceEvent +}).get() + + +export const isTriggerMessageHandlerEvent = tg.isUnion(isTriggerMessageEventObject, isTriggerMessageRemoveEventObject) + + + + +export function triggerMessageEventHandler(event: tg.GuardedType) { + if (isTriggerMessageEventObject(event)) { + _triggerMessageEvent.next(event.data) + } else if (isTriggerMessageRemoveEventObject(event)) { + _removeTriggerMessageEvent.next(event.data) + } +} \ No newline at end of file diff --git a/front/src/Api/IframeListener.ts b/front/src/Api/IframeListener.ts index 9311d7b6..3320519a 100644 --- a/front/src/Api/IframeListener.ts +++ b/front/src/Api/IframeListener.ts @@ -1,14 +1,14 @@ -import {Subject} from "rxjs"; -import {ChatEvent, isChatEvent} from "./Events/ChatEvent"; -import {HtmlUtils} from "../WebRtc/HtmlUtils"; -import type {EnterLeaveEvent} from "./Events/EnterLeaveEvent"; -import {isOpenPopupEvent, OpenPopupEvent} from "./Events/OpenPopupEvent"; -import {isOpenTabEvent, OpenTabEvent} from "./Events/OpenTabEvent"; -import type {ButtonClickedEvent} from "./Events/ButtonClickedEvent"; -import {ClosePopupEvent, isClosePopupEvent} from "./Events/ClosePopupEvent"; -import {scriptUtils} from "./ScriptUtils"; -import {GoToPageEvent, isGoToPageEvent} from "./Events/GoToPageEvent"; -import {isOpenCoWebsite, OpenCoWebSiteEvent} from "./Events/OpenCoWebSiteEvent"; +import { Subject } from "rxjs"; +import { ChatEvent, isChatEvent } from "./Events/ChatEvent"; +import { HtmlUtils } from "../WebRtc/HtmlUtils"; +import type { EnterLeaveEvent } from "./Events/EnterLeaveEvent"; +import { isOpenPopupEvent, OpenPopupEvent } from "./Events/OpenPopupEvent"; +import { isOpenTabEvent, OpenTabEvent } from "./Events/OpenTabEvent"; +import type { ButtonClickedEvent } from "./Events/ButtonClickedEvent"; +import { ClosePopupEvent, isClosePopupEvent } from "./Events/ClosePopupEvent"; +import { scriptUtils } from "./ScriptUtils"; +import { GoToPageEvent, isGoToPageEvent } from "./Events/GoToPageEvent"; +import { isOpenCoWebsite, OpenCoWebSiteEvent } from "./Events/OpenCoWebSiteEvent"; import { IframeEvent, IframeEventMap, @@ -17,19 +17,20 @@ import { isIframeEventWrapper, TypedMessageEvent } from "./Events/IframeEvent"; -import type {UserInputChatEvent} from "./Events/UserInputChatEvent"; +import type { UserInputChatEvent } from "./Events/UserInputChatEvent"; //import { isLoadPageEvent } from './Events/LoadPageEvent'; -import {isPlaySoundEvent, PlaySoundEvent} from "./Events/PlaySoundEvent"; -import {isStopSoundEvent, StopSoundEvent} from "./Events/StopSoundEvent"; -import {isLoadSoundEvent, LoadSoundEvent} from "./Events/LoadSoundEvent"; -import {isSetPropertyEvent, SetPropertyEvent} from "./Events/setPropertyEvent"; -import {isLayerEvent, LayerEvent} from "./Events/LayerEvent"; -import {isMenuItemRegisterEvent,} from "./Events/ui/MenuItemRegisterEvent"; -import type {DataLayerEvent} from "./Events/DataLayerEvent"; -import type {GameStateEvent} from "./Events/GameStateEvent"; -import type {HasPlayerMovedEvent} from "./Events/HasPlayerMovedEvent"; -import {isLoadPageEvent} from "./Events/LoadPageEvent"; -import {handleMenuItemRegistrationEvent, isMenuItemRegisterIframeEvent} from "./Events/ui/MenuItemRegisterEvent"; +import { isPlaySoundEvent, PlaySoundEvent } from "./Events/PlaySoundEvent"; +import { isStopSoundEvent, StopSoundEvent } from "./Events/StopSoundEvent"; +import { isLoadSoundEvent, LoadSoundEvent } from "./Events/LoadSoundEvent"; +import { isSetPropertyEvent, SetPropertyEvent } from "./Events/setPropertyEvent"; +import { isLayerEvent, LayerEvent } from "./Events/LayerEvent"; +import { isMenuItemRegisterEvent, } from "./Events/ui/MenuItemRegisterEvent"; +import type { DataLayerEvent } from "./Events/DataLayerEvent"; +import type { GameStateEvent } from "./Events/GameStateEvent"; +import type { HasPlayerMovedEvent } from "./Events/HasPlayerMovedEvent"; +import { isLoadPageEvent } from "./Events/LoadPageEvent"; +import { handleMenuItemRegistrationEvent, isMenuItemRegisterIframeEvent } from "./Events/ui/MenuItemRegisterEvent"; +import { isTriggerMessageHandlerEvent, triggerMessageEventHandler } from './Events/ui/TriggerMessageEventHandler'; /** * Listens to messages from iframes and turn those messages into easy to use observables. @@ -190,6 +191,8 @@ class IframeListener { this._unregisterMenuCommandStream.next(data); }) handleMenuItemRegistrationEvent(payload.data) + } else if (isTriggerMessageHandlerEvent(payload)) { + triggerMessageEventHandler(payload) } } }, false); @@ -198,8 +201,8 @@ class IframeListener { sendDataLayerEvent(dataLayerEvent: DataLayerEvent) { this.postMessage({ - 'type' : 'dataLayer', - 'data' : dataLayerEvent + 'type': 'dataLayer', + 'data': dataLayerEvent }) } diff --git a/front/src/Api/iframe/Ui/TriggerMessage.ts b/front/src/Api/iframe/Ui/TriggerMessage.ts new file mode 100644 index 00000000..af0e20ce --- /dev/null +++ b/front/src/Api/iframe/Ui/TriggerMessage.ts @@ -0,0 +1,51 @@ + +import { removeTriggerMessage, triggerMessage, TriggerMessageEvent } from '../../Events/ui/TriggerMessageEvent'; +import { sendToWorkadventure } from '../IframeApiContribution'; +function uuidv4() { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { + const r = Math.random() * 16 | 0, v = c === 'x' ? r : (r & 0x3 | 0x8); + return v.toString(16); + }); +} + +export let triggerMessageInstance: TriggerMessage | undefined = undefined + + + +export class TriggerMessage { + uuid: string + + constructor(private message: string, private callback: () => void) { + this.uuid = uuidv4() + if (triggerMessageInstance) { + triggerMessageInstance.remove(); + } + triggerMessageInstance = this; + this.create(); + } + + create(): this { + sendToWorkadventure({ + type: triggerMessage, + data: { + message: this.message, + uuid: this.uuid + } as TriggerMessageEvent + }) + return this + } + + remove() { + sendToWorkadventure({ + type: removeTriggerMessage, + data: { + uuid: this.uuid + } as TriggerMessageEvent + }) + triggerMessageInstance = undefined + } + + trigger() { + this.callback(); + } +} \ No newline at end of file diff --git a/front/src/Api/iframe/ui.ts b/front/src/Api/iframe/ui.ts index c7655b84..834cc347 100644 --- a/front/src/Api/iframe/ui.ts +++ b/front/src/Api/iframe/ui.ts @@ -1,10 +1,11 @@ import { isButtonClickedEvent } from '../Events/ButtonClickedEvent'; import { isMenuItemClickedEvent } from '../Events/ui/MenuItemClickedEvent'; -import type { MenuItemRegisterEvent } from '../Events/ui/MenuItemRegisterEvent'; +import { isMessageReferenceEvent } from '../Events/ui/TriggerMessageEvent'; import { IframeApiContribution, sendToWorkadventure } from './IframeApiContribution'; import { apiCallback } from "./registeredCallbacks"; import type { ButtonClickedCallback, ButtonDescriptor } from "./Ui/ButtonDescriptor"; import { Popup } from "./Ui/Popup"; +import { TriggerMessage, triggerMessageInstance } from './Ui/TriggerMessage'; let popupId = 0; const popups: Map = new Map(); @@ -12,41 +13,41 @@ const popupCallbacks: Map> = new Map< const menuCallbacks: Map void> = new Map() -interface ZonedPopupOptions { - zone: string - objectLayerName?: string, - popupText: string, - delay?: number - popupOptions: Array -} - - class WorkAdventureUiCommands extends IframeApiContribution { - callbacks = [apiCallback({ - type: "buttonClickedEvent", - typeChecker: isButtonClickedEvent, - callback: (payloadData) => { - const callback = popupCallbacks.get(payloadData.popupId)?.get(payloadData.buttonId); - const popup = popups.get(payloadData.popupId); - if (popup === undefined) { - throw new Error('Could not find popup with ID "' + payloadData.popupId + '"'); + callbacks = [ + apiCallback({ + type: "buttonClickedEvent", + typeChecker: isButtonClickedEvent, + callback: (payloadData) => { + const callback = popupCallbacks.get(payloadData.popupId)?.get(payloadData.buttonId); + const popup = popups.get(payloadData.popupId); + if (popup === undefined) { + throw new Error('Could not find popup with ID "' + payloadData.popupId + '"'); + } + if (callback) { + callback(popup); + } } - if (callback) { - callback(popup); + }), + apiCallback({ + type: "menuItemClicked", + typeChecker: isMenuItemClickedEvent, + callback: event => { + const callback = menuCallbacks.get(event.menuItem); + if (callback) { + callback(event.menuItem) + } } - } - }), - apiCallback({ - type: "menuItemClicked", - typeChecker: isMenuItemClickedEvent, - callback: event => { - const callback = menuCallbacks.get(event.menuItem); - if (callback) { - callback(event.menuItem) + }), + apiCallback({ + type: "messageTriggered", + typeChecker: isMessageReferenceEvent, + callback: event => { + triggerMessageInstance?.trigger(); } - } - })]; + }) + ]; openPopup(targetObject: string, message: string, buttons: ButtonDescriptor[]): Popup { @@ -101,6 +102,9 @@ class WorkAdventureUiCommands extends IframeApiContribution void): TriggerMessage { + return new TriggerMessage(message, callback); + } } export default new WorkAdventureUiCommands(); diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 7c07f187..2cb4a363 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -64,7 +64,8 @@ import type { ITiledMapLayerProperty, ITiledMapObject, ITiledMapTileLayer, - ITiledTileSet } from "../Map/ITiledMap"; + ITiledTileSet +} from "../Map/ITiledMap"; import { MenuScene, MenuSceneName } from '../Menu/MenuScene'; import { PlayerAnimationDirections } from "../Player/Animation"; import { hasMovedEventName, Player, requestEmoteEventName } from "../Player/Player"; @@ -93,7 +94,8 @@ import Tilemap = Phaser.Tilemaps.Tilemap; import type { HasPlayerMovedEvent } from '../../Api/Events/HasPlayerMovedEvent'; import AnimatedTiles from "phaser-animated-tiles"; -import {soundManager} from "./SoundManager"; +import { soundManager } from "./SoundManager"; +import { removeTriggerMessageEvent, sendMessageTriggeredEvent, triggerMessageEvent } from '../../Api/Events/ui/TriggerMessageEventHandler'; export interface GameSceneInitInterface { initPosition: PointInterface | null, @@ -932,11 +934,11 @@ ${escapedMessage} scriptedBubbleSprite.destroy(); })); - this.iframeSubscriptionList.push(iframeListener.showLayerStream.subscribe((layerEvent)=>{ + this.iframeSubscriptionList.push(iframeListener.showLayerStream.subscribe((layerEvent) => { this.setLayerVisibility(layerEvent.name, true); })); - this.iframeSubscriptionList.push(iframeListener.hideLayerStream.subscribe((layerEvent)=>{ + this.iframeSubscriptionList.push(iframeListener.hideLayerStream.subscribe((layerEvent) => { this.setLayerVisibility(layerEvent.name, false); })); @@ -945,7 +947,7 @@ ${escapedMessage} })); this.iframeSubscriptionList.push(iframeListener.dataLayerChangeStream.subscribe(() => { - iframeListener.sendDataLayerEvent({data: this.gameMap.getMap()}); + iframeListener.sendDataLayerEvent({ data: this.gameMap.getMap() }); })) this.iframeSubscriptionList.push(iframeListener.gameStateStream.subscribe(() => { @@ -959,21 +961,33 @@ ${escapedMessage} }) })); + + this.iframeSubscriptionList.push(triggerMessageEvent.subscribe(message => { + layoutManager.addActionButton(message.uuid, message.message, () => { + sendMessageTriggeredEvent(message.uuid) + layoutManager.removeActionButton(message.uuid, this.userInputManager); + }, this.userInputManager); + })) + + this.iframeSubscriptionList.push(removeTriggerMessageEvent.subscribe(message => { + layoutManager.removeActionButton(message.uuid, this.userInputManager); + })) + } private setPropertyLayer(layerName: string, propertyName: string, propertyValue: string | number | boolean | undefined): void { const layer = this.gameMap.findLayer(layerName); - if (layer === undefined) { + if (layer === undefined) { console.warn('Could not find layer "' + layerName + '" when calling setProperty'); return; } - const property = (layer.properties as ITiledMapLayerProperty[])?.find((property) => property.name === propertyName); - if (property === undefined) { - layer.properties = []; - layer.properties.push({name : propertyName, type : typeof propertyValue, value : propertyValue}); - return; - } - property.value = propertyValue; + const property = (layer.properties as ITiledMapLayerProperty[])?.find((property) => property.name === propertyName); + if (property === undefined) { + layer.properties = []; + layer.properties.push({ name: propertyName, type: typeof propertyValue, value: propertyValue }); + return; + } + property.value = propertyValue; } private setLayerVisibility(layerName: string, visible: boolean): void { @@ -1150,7 +1164,7 @@ ${escapedMessage} } //todo: push that into the gameManager - private loadNextGame(exitSceneIdentifier: string) : Promise{ + private loadNextGame(exitSceneIdentifier: string): Promise { const { roomId, hash } = Room.getIdFromIdentifier(exitSceneIdentifier, this.MapUrlFile, this.instance); const room = new Room(roomId); return gameManager.loadMap(room, this.scene).catch(() => { }); @@ -1197,7 +1211,7 @@ ${escapedMessage} this.physics.add.collider(this.CurrentPlayer, phaserLayer, (object1: GameObject, object2: GameObject) => { //this.CurrentPlayer.say("Collision with layer : "+ (object2 as Tile).layer.name) }); - phaserLayer.setCollisionByProperty({collides: true}); + phaserLayer.setCollisionByProperty({ collides: true }); if (DEBUG_MODE) { //debug code to see the collision hitbox of the object in the top layer phaserLayer.renderDebug(this.add.graphics(), { @@ -1206,7 +1220,7 @@ ${escapedMessage} faceColor: new Phaser.Display.Color(40, 39, 37, 255) // Colliding face edges }); } - //}); + //}); } } } diff --git a/maps/tests/script.js b/maps/tests/script.js index b300700f..ac3541f6 100644 --- a/maps/tests/script.js +++ b/maps/tests/script.js @@ -1,40 +1,41 @@ +/// console.log('SCRIPT LAUNCHED'); //WA.sendChatMessage('Hi, my name is Poly and I repeat what you say!', 'Poly Parrot'); var isFirstTimeTuto = false; var textFirstPopup = 'Hey ! This is how to open start a discussion with someone ! You can be 4 max in a booble'; var textSecondPopup = 'You can also use the chat to communicate ! '; -var targetObjectTutoBubble ='myPopup1'; -var targetObjectTutoChat ='myPopup2'; +var targetObjectTutoBubble = 'myPopup1'; +var targetObjectTutoChat = 'myPopup2'; var popUpExplanation = undefined; -function launchTuto (){ - WA.ui.openPopup(targetObjectTutoBubble, textFirstPopup, [ - { - label: "Next", - className: "popUpElement", - callback: (popup) => { - popup.close(); +function launchTuto() { + WA.ui.openPopup(targetObjectTutoBubble, textFirstPopup, [ + { + label: "Next", + className: "popUpElement", + callback: (popup) => { + popup.close(); - WA.ui.openPopup(targetObjectTutoChat, textSecondPopup, [ - { - label: "Open Chat", - className: "popUpElement", - callback: (popup1) => { - WA.chat.sendChatMessage("Hey you can talk here too ! ", 'WA Guide'); - popup1.close(); - WA.controls.restorePlayerControls(); - } + WA.ui.openPopup(targetObjectTutoChat, textSecondPopup, [ + { + label: "Open Chat", + className: "popUpElement", + callback: (popup1) => { + WA.chat.sendChatMessage("Hey you can talk here too ! ", 'WA Guide'); + popup1.close(); + WA.controls.restorePlayerControls(); } + } - ]) - } + ]) } - ]); - WA.controls.disablePlayerControls(); + } + ]); + WA.controls.disablePlayerControls(); } WA.chat.onChatMessage((message => { console.log('CHAT MESSAGE RECEIVED BY SCRIPT'); - WA.chat.sendChatMessage('Poly Parrot says: "'+message+'"', 'Poly Parrot'); + WA.chat.sendChatMessage('Poly Parrot says: "' + message + '"', 'Poly Parrot'); })); WA.room.onEnterZone('myTrigger', () => { @@ -50,11 +51,11 @@ WA.room.onEnterZone('notExist', () => { WA.room.onEnterZone('popupZone', () => { WA.ui.displayBubble(); - if (!isFirstTimeTuto) { + if(!isFirstTimeTuto) { isFirstTimeTuto = true; launchTuto(); } - else popUpExplanation = WA.ui.openPopup(targetObjectTutoChat,'Do you want to review the explanation ? ', [ + else popUpExplanation = WA.ui.openPopup(targetObjectTutoChat, 'Do you want to review the explanation ? ', [ { label: "No", className: "popUpElementReviewexplanation", @@ -74,6 +75,13 @@ WA.room.onEnterZone('popupZone', () => { }); WA.room.onLeaveZone('popupZone', () => { - if (popUpExplanation !== undefined) popUpExplanation.close(); + if(popUpExplanation !== undefined) popUpExplanation.close(); WA.ui.removeBubble(); }) + +const message = WA.ui.triggerMessage("testMessage", () => { + WA.chat.sendChatMessage("triggered", "triggerbot"); +}) +setTimeout(() => { + message.remove(); +}, 5000) \ No newline at end of file From 9643c7adf92c5e0a8899a5d9ae287ec4344235ab Mon Sep 17 00:00:00 2001 From: jonny Date: Mon, 28 Jun 2021 11:16:29 +0200 Subject: [PATCH 002/106] added callback documentation --- docs/maps/api-ui.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/maps/api-ui.md b/docs/maps/api-ui.md index b1d244da..fda7742d 100644 --- a/docs/maps/api-ui.md +++ b/docs/maps/api-ui.md @@ -93,7 +93,7 @@ WA.ui.registerMenuCommand("test", () => { ### Awaiting User Confirmation (with space bar) ```typescript -triggerMessage(message: string): TriggerMessage +triggerMessage(message: string, callback: ()=>void): TriggerMessage ``` Displays a message at the bottom of the screen (that will disappear when space bar is pressed). From 917f3728d559f96898e750e809df12daed9642b6 Mon Sep 17 00:00:00 2001 From: jonny Date: Mon, 28 Jun 2021 11:17:22 +0200 Subject: [PATCH 003/106] added callback in example --- docs/maps/api-ui.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/maps/api-ui.md b/docs/maps/api-ui.md index fda7742d..db35f5b8 100644 --- a/docs/maps/api-ui.md +++ b/docs/maps/api-ui.md @@ -101,7 +101,9 @@ Displays a message at the bottom of the screen (that will disappear when space b Example: ```javascript -const triggerMessage = WA.ui.triggerMessage("press 'space' to confirm"); +const triggerMessage = WA.ui.triggerMessage("press 'space' to confirm",()=>{ + WA.chat.sendChatMessage("confirmed", "trigger message logic") +}); setTimeout(()=>{ // later triggerMessage.remove(); 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 004/106] 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 005/106] 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 + + \n' + '\n' + - "\n" + - "\n" + - "\n"; + '\n' + + '\n' + + '\n'; document.body.prepend(iframe); @@ -353,7 +353,7 @@ class IframeListener { } private static getIFrameId(scriptUrl: string): string { - return "script" + btoa(scriptUrl); + return 'script' + btoa(scriptUrl); } unregisterScript(scriptUrl: string): void { @@ -370,7 +370,7 @@ class IframeListener { sendUserInputChat(message: string) { this.postMessage({ - type: "userInputChat", + type: 'userInputChat', data: { message: message, } as UserInputChatEvent, @@ -379,7 +379,7 @@ class IframeListener { sendEnterEvent(name: string) { this.postMessage({ - type: "enterEvent", + type: 'enterEvent', data: { name: name, } as EnterLeaveEvent, @@ -388,7 +388,7 @@ class IframeListener { sendLeaveEvent(name: string) { this.postMessage({ - type: "leaveEvent", + type: 'leaveEvent', data: { name: name, } as EnterLeaveEvent, @@ -398,7 +398,7 @@ class IframeListener { hasPlayerMoved(event: HasPlayerMovedEvent) { if (this.sendPlayerMove) { this.postMessage({ - type: "hasPlayerMoved", + type: 'hasPlayerMoved', data: event, }); } @@ -406,7 +406,7 @@ class IframeListener { sendButtonClickedEvent(popupId: number, buttonId: number): void { this.postMessage({ - type: "buttonClickedEvent", + type: 'buttonClickedEvent', data: { popupId, buttonId, @@ -419,7 +419,7 @@ class IframeListener { */ public postMessage(message: IframeResponseEvent) { for (const iframe of this.iframes) { - iframe.contentWindow?.postMessage(message, "*"); + iframe.contentWindow?.postMessage(message, '*'); } } @@ -431,7 +431,7 @@ class IframeListener { * @param key The "type" of the query we are answering * @param callback */ - public registerAnswerer>( + public registerAnswerer>( key: T, callback: AnswererCallback, typeChecker?: Guard diff --git a/front/src/Api/iframe/Ui/TriggerMessage.ts b/front/src/Api/iframe/Ui/TriggerMessage.ts index 333e6992..3afc0064 100644 --- a/front/src/Api/iframe/Ui/TriggerMessage.ts +++ b/front/src/Api/iframe/Ui/TriggerMessage.ts @@ -3,12 +3,12 @@ import { removeTriggerMessage, triggerMessage, TriggerMessageEvent, -} from "../../Events/ui/TriggerMessageEvent"; -import { queryWorkadventure } from "../IframeApiContribution"; +} from '../../Events/ui/TriggerMessageEvent'; +import { queryWorkadventure } from '../IframeApiContribution'; function uuidv4() { - return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { const r = (Math.random() * 16) | 0, - v = c === "x" ? r : (r & 0x3) | 0x8; + v = c === 'x' ? r : (r & 0x3) | 0x8; return v.toString(16); }); } diff --git a/front/src/Api/iframe/ui.ts b/front/src/Api/iframe/ui.ts index c1fa85b5..302a9eff 100644 --- a/front/src/Api/iframe/ui.ts +++ b/front/src/Api/iframe/ui.ts @@ -1,10 +1,10 @@ -import { isButtonClickedEvent } from "../Events/ButtonClickedEvent"; -import { isMenuItemClickedEvent } from "../Events/ui/MenuItemClickedEvent"; -import { IframeApiContribution, sendToWorkadventure } from "./IframeApiContribution"; -import { apiCallback } from "./registeredCallbacks"; -import type { ButtonClickedCallback, ButtonDescriptor } from "./Ui/ButtonDescriptor"; -import { Popup } from "./Ui/Popup"; -import { TriggerMessage } from "./Ui/TriggerMessage"; +import { isButtonClickedEvent } from '../Events/ButtonClickedEvent'; +import { isMenuItemClickedEvent } from '../Events/ui/MenuItemClickedEvent'; +import { IframeApiContribution, sendToWorkadventure } from './IframeApiContribution'; +import { apiCallback } from './registeredCallbacks'; +import type { ButtonClickedCallback, ButtonDescriptor } from './Ui/ButtonDescriptor'; +import { Popup } from './Ui/Popup'; +import { TriggerMessage } from './Ui/TriggerMessage'; let popupId = 0; const popups: Map = new Map(); @@ -26,7 +26,7 @@ interface ZonedPopupOptions { export class WorkAdventureUiCommands extends IframeApiContribution { callbacks = [ apiCallback({ - type: "buttonClickedEvent", + type: 'buttonClickedEvent', typeChecker: isButtonClickedEvent, callback: (payloadData) => { const callback = popupCallbacks.get(payloadData.popupId)?.get(payloadData.buttonId); @@ -40,7 +40,7 @@ export class WorkAdventureUiCommands extends IframeApiContribution { const callback = menuCallbacks.get(event.menuItem); @@ -68,7 +68,7 @@ export class WorkAdventureUiCommands extends IframeApiContribution void) { menuCallbacks.set(commandDescriptor, callback); sendToWorkadventure({ - type: "registerMenuCommand", + type: 'registerMenuCommand', data: { menutItem: commandDescriptor, }, @@ -97,11 +97,11 @@ export class WorkAdventureUiCommands extends IframeApiContribution void): TriggerMessage { diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 530a31e2..17f0594f 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -1,8 +1,8 @@ -import type { Subscription } from "rxjs"; -import { GlobalMessageManager } from "../../Administration/GlobalMessageManager"; -import { userMessageManager } from "../../Administration/UserMessageManager"; -import { iframeListener } from "../../Api/IframeListener"; -import { connectionManager } from "../../Connexion/ConnectionManager"; +import type { Subscription } from 'rxjs'; +import { GlobalMessageManager } from '../../Administration/GlobalMessageManager'; +import { userMessageManager } from '../../Administration/UserMessageManager'; +import { iframeListener } from '../../Api/IframeListener'; +import { connectionManager } from '../../Connexion/ConnectionManager'; import type { GroupCreatedUpdatedMessageInterface, MessageUserJoined, @@ -12,10 +12,10 @@ import type { PointInterface, PositionInterface, RoomJoinedMessageInterface, -} from "../../Connexion/ConnexionModels"; -import { DEBUG_MODE, JITSI_PRIVATE_MODE, MAX_PER_GROUP, POSITION_DELAY } from "../../Enum/EnvironmentVariable"; +} from '../../Connexion/ConnexionModels'; +import { DEBUG_MODE, JITSI_PRIVATE_MODE, MAX_PER_GROUP, POSITION_DELAY } from '../../Enum/EnvironmentVariable'; -import { Queue } from "queue-typescript"; +import { Queue } from 'queue-typescript'; import { AUDIO_LOOP_PROPERTY, AUDIO_VOLUME_PROPERTY, @@ -26,72 +26,72 @@ import { TRIGGER_JITSI_PROPERTIES, TRIGGER_WEBSITE_PROPERTIES, WEBSITE_MESSAGE_PROPERTIES, -} from "../../WebRtc/LayoutManager"; -import { coWebsiteManager } from "../../WebRtc/CoWebsiteManager"; -import type { UserMovedMessage } from "../../Messages/generated/messages_pb"; -import { ProtobufClientUtils } from "../../Network/ProtobufClientUtils"; -import type { RoomConnection } from "../../Connexion/RoomConnection"; -import { Room } from "../../Connexion/Room"; -import { jitsiFactory } from "../../WebRtc/JitsiFactory"; -import { urlManager } from "../../Url/UrlManager"; -import { audioManager } from "../../WebRtc/AudioManager"; -import { TextureError } from "../../Exception/TextureError"; -import { localUserStore } from "../../Connexion/LocalUserStore"; -import { HtmlUtils } from "../../WebRtc/HtmlUtils"; -import { mediaManager } from "../../WebRtc/MediaManager"; -import { SimplePeer } from "../../WebRtc/SimplePeer"; -import { addLoader } from "../Components/Loader"; -import { OpenChatIcon, openChatIconName } from "../Components/OpenChatIcon"; -import { lazyLoadPlayerCharacterTextures, loadCustomTexture } from "../Entity/PlayerTexturesLoadingManager"; -import { RemotePlayer } from "../Entity/RemotePlayer"; -import type { ActionableItem } from "../Items/ActionableItem"; -import type { ItemFactoryInterface } from "../Items/ItemFactoryInterface"; -import { SelectCharacterScene, SelectCharacterSceneName } from "../Login/SelectCharacterScene"; +} from '../../WebRtc/LayoutManager'; +import { coWebsiteManager } from '../../WebRtc/CoWebsiteManager'; +import type { UserMovedMessage } from '../../Messages/generated/messages_pb'; +import { ProtobufClientUtils } from '../../Network/ProtobufClientUtils'; +import type { RoomConnection } from '../../Connexion/RoomConnection'; +import { Room } from '../../Connexion/Room'; +import { jitsiFactory } from '../../WebRtc/JitsiFactory'; +import { urlManager } from '../../Url/UrlManager'; +import { audioManager } from '../../WebRtc/AudioManager'; +import { TextureError } from '../../Exception/TextureError'; +import { localUserStore } from '../../Connexion/LocalUserStore'; +import { HtmlUtils } from '../../WebRtc/HtmlUtils'; +import { mediaManager } from '../../WebRtc/MediaManager'; +import { SimplePeer } from '../../WebRtc/SimplePeer'; +import { addLoader } from '../Components/Loader'; +import { OpenChatIcon, openChatIconName } from '../Components/OpenChatIcon'; +import { lazyLoadPlayerCharacterTextures, loadCustomTexture } from '../Entity/PlayerTexturesLoadingManager'; +import { RemotePlayer } from '../Entity/RemotePlayer'; +import type { ActionableItem } from '../Items/ActionableItem'; +import type { ItemFactoryInterface } from '../Items/ItemFactoryInterface'; +import { SelectCharacterScene, SelectCharacterSceneName } from '../Login/SelectCharacterScene'; import type { ITiledMap, ITiledMapLayer, ITiledMapLayerProperty, ITiledMapObject, ITiledTileSet, -} from "../Map/ITiledMap"; -import { MenuScene, MenuSceneName } from "../Menu/MenuScene"; -import { PlayerAnimationDirections } from "../Player/Animation"; -import { hasMovedEventName, Player, requestEmoteEventName } from "../Player/Player"; -import { ErrorSceneName } from "../Reconnecting/ErrorScene"; -import { ReconnectingSceneName } from "../Reconnecting/ReconnectingScene"; -import { UserInputManager } from "../UserInput/UserInputManager"; -import type { AddPlayerInterface } from "./AddPlayerInterface"; -import { gameManager } from "./GameManager"; -import { GameMap } from "./GameMap"; -import { PlayerMovement } from "./PlayerMovement"; -import { PlayersPositionInterpolator } from "./PlayersPositionInterpolator"; +} from '../Map/ITiledMap'; +import { MenuScene, MenuSceneName } from '../Menu/MenuScene'; +import { PlayerAnimationDirections } from '../Player/Animation'; +import { hasMovedEventName, Player, requestEmoteEventName } from '../Player/Player'; +import { ErrorSceneName } from '../Reconnecting/ErrorScene'; +import { ReconnectingSceneName } from '../Reconnecting/ReconnectingScene'; +import { UserInputManager } from '../UserInput/UserInputManager'; +import type { AddPlayerInterface } from './AddPlayerInterface'; +import { gameManager } from './GameManager'; +import { GameMap } from './GameMap'; +import { PlayerMovement } from './PlayerMovement'; +import { PlayersPositionInterpolator } from './PlayersPositionInterpolator'; import Texture = Phaser.Textures.Texture; import Sprite = Phaser.GameObjects.Sprite; import CanvasTexture = Phaser.Textures.CanvasTexture; import GameObject = Phaser.GameObjects.GameObject; import FILE_LOAD_ERROR = Phaser.Loader.Events.FILE_LOAD_ERROR; import DOMElement = Phaser.GameObjects.DOMElement; -import { worldFullMessageStream } from "../../Connexion/WorldFullMessageStream"; -import { lazyLoadCompanionResource } from "../Companion/CompanionTexturesLoadingManager"; -import { DirtyScene } from "./DirtyScene"; -import { TextUtils } from "../Components/TextUtils"; -import { touchScreenManager } from "../../Touch/TouchScreenManager"; -import { PinchManager } from "../UserInput/PinchManager"; -import { joystickBaseImg, joystickBaseKey, joystickThumbImg, joystickThumbKey } from "../Components/MobileJoystick"; -import { waScaleManager } from "../Services/WaScaleManager"; -import { EmoteManager } from "./EmoteManager"; +import { worldFullMessageStream } from '../../Connexion/WorldFullMessageStream'; +import { lazyLoadCompanionResource } from '../Companion/CompanionTexturesLoadingManager'; +import { DirtyScene } from './DirtyScene'; +import { TextUtils } from '../Components/TextUtils'; +import { touchScreenManager } from '../../Touch/TouchScreenManager'; +import { PinchManager } from '../UserInput/PinchManager'; +import { joystickBaseImg, joystickBaseKey, joystickThumbImg, joystickThumbKey } from '../Components/MobileJoystick'; +import { waScaleManager } from '../Services/WaScaleManager'; +import { EmoteManager } from './EmoteManager'; import EVENT_TYPE = Phaser.Scenes.Events; import RenderTexture = Phaser.GameObjects.RenderTexture; import Tilemap = Phaser.Tilemaps.Tilemap; -import type { HasPlayerMovedEvent } from "../../Api/Events/HasPlayerMovedEvent"; +import type { HasPlayerMovedEvent } from '../../Api/Events/HasPlayerMovedEvent'; -import AnimatedTiles from "phaser-animated-tiles"; -import { StartPositionCalculator } from "./StartPositionCalculator"; -import { soundManager } from "./SoundManager"; -import { peerStore, screenSharingPeerStore } from "../../Stores/PeerStore"; -import { videoFocusStore } from "../../Stores/VideoFocusStore"; -import { biggestAvailableAreaStore } from "../../Stores/BiggestAvailableAreaStore"; -import { isMessageReferenceEvent, isTriggerMessageEvent } from "../../Api/Events/ui/TriggerMessageEvent"; +import AnimatedTiles from 'phaser-animated-tiles'; +import { StartPositionCalculator } from './StartPositionCalculator'; +import { soundManager } from './SoundManager'; +import { peerStore, screenSharingPeerStore } from '../../Stores/PeerStore'; +import { videoFocusStore } from '../../Stores/VideoFocusStore'; +import { biggestAvailableAreaStore } from '../../Stores/BiggestAvailableAreaStore'; +import { isMessageReferenceEvent, isTriggerMessageEvent } from '../../Api/Events/ui/TriggerMessageEvent'; export interface GameSceneInitInterface { initPosition: PointInterface | null; @@ -99,32 +99,32 @@ export interface GameSceneInitInterface { } interface InitUserPositionEventInterface { - type: "InitUserPositionEvent"; + type: 'InitUserPositionEvent'; event: MessageUserPositionInterface[]; } interface AddPlayerEventInterface { - type: "AddPlayerEvent"; + type: 'AddPlayerEvent'; event: AddPlayerInterface; } interface RemovePlayerEventInterface { - type: "RemovePlayerEvent"; + type: 'RemovePlayerEvent'; userId: number; } interface UserMovedEventInterface { - type: "UserMovedEvent"; + type: 'UserMovedEvent'; event: MessageUserMovedInterface; } interface GroupCreatedUpdatedEventInterface { - type: "GroupCreatedUpdatedEvent"; + type: 'GroupCreatedUpdatedEvent'; event: GroupCreatedUpdatedMessageInterface; } interface DeleteGroupEventInterface { - type: "DeleteGroupEvent"; + type: 'DeleteGroupEvent'; groupId: number; } @@ -177,7 +177,7 @@ export class GameScene extends DirtyScene { currentTick!: number; lastSentTick!: number; // The last tick at which a position was sent. lastMoveEventSent: HasPlayerMovedEvent = { - direction: "", + direction: '', moving: false, x: -1000, y: -1000, @@ -231,29 +231,29 @@ export class GameScene extends DirtyScene { } } - this.load.image(openChatIconName, "resources/objects/talk.png"); + this.load.image(openChatIconName, 'resources/objects/talk.png'); if (touchScreenManager.supportTouchScreen) { this.load.image(joystickBaseKey, joystickBaseImg); this.load.image(joystickThumbKey, joystickThumbImg); } - this.load.audio("audio-webrtc-in", "/resources/objects/webrtc-in.mp3"); - this.load.audio("audio-webrtc-out", "/resources/objects/webrtc-out.mp3"); + this.load.audio('audio-webrtc-in', '/resources/objects/webrtc-in.mp3'); + this.load.audio('audio-webrtc-out', '/resources/objects/webrtc-out.mp3'); //this.load.audio('audio-report-message', '/resources/objects/report-message.mp3'); this.sound.pauseOnBlur = false; this.load.on(FILE_LOAD_ERROR, (file: { src: string }) => { // If we happen to be in HTTP and we are trying to load a URL in HTTPS only... (this happens only in dev environments) if ( - window.location.protocol === "http:" && + window.location.protocol === 'http:' && file.src === this.MapUrlFile && - file.src.startsWith("http:") && + file.src.startsWith('http:') && this.originalMapUrl === undefined ) { this.originalMapUrl = this.MapUrlFile; - this.MapUrlFile = this.MapUrlFile.replace("http://", "https://"); + this.MapUrlFile = this.MapUrlFile.replace('http://', 'https://'); this.load.tilemapTiledJSON(this.MapUrlFile, this.MapUrlFile); this.load.on( - "filecomplete-tilemapJSON-" + this.MapUrlFile, + 'filecomplete-tilemapJSON-' + this.MapUrlFile, (key: string, type: string, data: unknown) => { this.onMapLoad(data); } @@ -264,18 +264,18 @@ export class GameScene extends DirtyScene { // So if we are in https, we can still try to load a HTTP local resource (can be useful for testing purposes) // See https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts#when_is_a_context_considered_secure const url = new URL(file.src); - const host = url.host.split(":")[0]; + const host = url.host.split(':')[0]; if ( - window.location.protocol === "https:" && + window.location.protocol === 'https:' && file.src === this.MapUrlFile && - (host === "127.0.0.1" || host === "localhost" || host.endsWith(".localhost")) && + (host === '127.0.0.1' || host === 'localhost' || host.endsWith('.localhost')) && this.originalMapUrl === undefined ) { this.originalMapUrl = this.MapUrlFile; - this.MapUrlFile = this.MapUrlFile.replace("https://", "http://"); + this.MapUrlFile = this.MapUrlFile.replace('https://', 'http://'); this.load.tilemapTiledJSON(this.MapUrlFile, this.MapUrlFile); this.load.on( - "filecomplete-tilemapJSON-" + this.MapUrlFile, + 'filecomplete-tilemapJSON-' + this.MapUrlFile, (key: string, type: string, data: unknown) => { this.onMapLoad(data); } @@ -284,17 +284,17 @@ export class GameScene extends DirtyScene { } //once preloading is over, we don't want loading errors to crash the game, so we need to disable this behavior after preloading. - console.error("Error when loading: ", file); + console.error('Error when loading: ', file); if (this.preloading) { this.scene.start(ErrorSceneName, { - title: "Network error", - subTitle: "An error occurred while loading resource:", + title: 'Network error', + subTitle: 'An error occurred while loading resource:', message: this.originalMapUrl ?? file.src, }); } }); - this.load.scenePlugin("AnimatedTiles", AnimatedTiles, "animatedTiles", "animatedTiles"); - this.load.on("filecomplete-tilemapJSON-" + this.MapUrlFile, (key: string, type: string, data: unknown) => { + this.load.scenePlugin('AnimatedTiles', AnimatedTiles, 'animatedTiles', 'animatedTiles'); + this.load.on('filecomplete-tilemapJSON-' + this.MapUrlFile, (key: string, type: string, data: unknown) => { this.onMapLoad(data); }); //TODO strategy to add access token @@ -306,13 +306,13 @@ export class GameScene extends DirtyScene { this.onMapLoad(data); } - this.load.bitmapFont("main_font", "resources/fonts/arcade.png", "resources/fonts/arcade.xml"); + this.load.bitmapFont('main_font', 'resources/fonts/arcade.png', 'resources/fonts/arcade.xml'); //eslint-disable-next-line @typescript-eslint/no-explicit-any (this.load as any).rexWebFont({ custom: { - families: ["Press Start 2P"], - urls: ["/resources/fonts/fonts.css"], - testString: "abcdefg", + families: ['Press Start 2P'], + urls: ['/resources/fonts/fonts.css'], + testString: 'abcdefg', }, }); @@ -326,9 +326,9 @@ export class GameScene extends DirtyScene { // Triggered when the map is loaded // Load tiles attached to the map recursively this.mapFile = data.data; - const url = this.MapUrlFile.substr(0, this.MapUrlFile.lastIndexOf("/")); + const url = this.MapUrlFile.substr(0, this.MapUrlFile.lastIndexOf('/')); this.mapFile.tilesets.forEach((tileset) => { - if (typeof tileset.name === "undefined" || typeof tileset.image === "undefined") { + if (typeof tileset.name === 'undefined' || typeof tileset.image === 'undefined') { console.warn("Don't know how to handle tileset ", tileset); return; } @@ -340,7 +340,7 @@ export class GameScene extends DirtyScene { const objects = new Map(); for (const layer of this.mapFile.layers) { - if (layer.type === "objectgroup") { + if (layer.type === 'objectgroup') { for (const object of layer.objects) { let objectsOfType: ITiledMapObject[] | undefined; if (!objects.has(object.type)) { @@ -348,7 +348,7 @@ export class GameScene extends DirtyScene { } else { objectsOfType = objects.get(object.type); if (objectsOfType === undefined) { - throw new Error("Unexpected object type not found"); + throw new Error('Unexpected object type not found'); } } objectsOfType.push(object); @@ -363,8 +363,8 @@ export class GameScene extends DirtyScene { let itemFactory: ItemFactoryInterface; switch (itemType) { - case "computer": { - const module = await import("../Items/Computer/computer"); + case 'computer': { + const module = await import('../Items/Computer/computer'); itemFactory = module.default; break; } @@ -376,7 +376,7 @@ export class GameScene extends DirtyScene { itemFactory.preload(this.load); this.load.start(); // Let's manually start the loader because the import might be over AFTER the loading ends. - this.load.on("complete", () => { + this.load.on('complete', () => { // FIXME: the factory might fail because the resources might not be loaded yet... // We would need to add a loader ended event in addition to the createPromise this.createPromise.then(async () => { @@ -432,7 +432,7 @@ export class GameScene extends DirtyScene { const playerName = gameManager.getPlayerName(); if (!playerName) { - throw "playerName is not set"; + throw 'playerName is not set'; } this.playerName = playerName; this.characterLayers = gameManager.getCharacterLayers(); @@ -440,7 +440,7 @@ export class GameScene extends DirtyScene { //initalise map this.Map = this.add.tilemap(this.MapUrlFile); - const mapDirUrl = this.MapUrlFile.substr(0, this.MapUrlFile.lastIndexOf("/")); + const mapDirUrl = this.MapUrlFile.substr(0, this.MapUrlFile.lastIndexOf('/')); this.mapFile.tilesets.forEach((tileset: ITiledTileSet) => { this.Terrains.push( this.Map.addTilesetImage( @@ -460,7 +460,7 @@ export class GameScene extends DirtyScene { //add layer on map this.gameMap = new GameMap(this.mapFile, this.Map, this.Terrains); for (const layer of this.gameMap.flatLayers) { - if (layer.type === "tilelayer") { + if (layer.type === 'tilelayer') { const exitSceneUrl = this.getExitSceneUrl(layer); if (exitSceneUrl !== undefined) { this.loadNextGame(exitSceneUrl); @@ -470,7 +470,7 @@ export class GameScene extends DirtyScene { this.loadNextGame(exitUrl); } } - if (layer.type === "objectgroup") { + if (layer.type === 'objectgroup') { for (const object of layer.objects) { if (object.text) { TextUtils.createTextFromITiledMapObject(this, object); @@ -501,7 +501,7 @@ export class GameScene extends DirtyScene { mediaManager.setUserInputManager(this.userInputManager); if (localUserStore.getFullscreen()) { - document.querySelector("body")?.requestFullscreen(); + document.querySelector('body')?.requestFullscreen(); } //notify game manager can to create currentUser in map @@ -511,7 +511,7 @@ export class GameScene extends DirtyScene { this.initCamera(); this.animatedTiles.init(this.Map); - this.events.on("tileanimationupdate", () => (this.dirty = true)); + this.events.on('tileanimationupdate', () => (this.dirty = true)); this.initCirclesCanvas(); @@ -561,11 +561,11 @@ export class GameScene extends DirtyScene { this.peerStoreUnsubscribe = peerStore.subscribe((peers) => { const newPeerNumber = peers.size; if (newPeerNumber > oldPeerNumber) { - this.sound.play("audio-webrtc-in", { + this.sound.play('audio-webrtc-in', { volume: 0.2, }); } else if (newPeerNumber < oldPeerNumber) { - this.sound.play("audio-webrtc-out", { + this.sound.play('audio-webrtc-out', { volume: 0.2, }); } @@ -613,7 +613,7 @@ export class GameScene extends DirtyScene { this.connection.onUserMoved((message: UserMovedMessage) => { const position = message.getPosition(); if (position === undefined) { - throw new Error("Position missing from UserMovedMessage"); + throw new Error('Position missing from UserMovedMessage'); } const messageUserMoved: MessageUserMovedInterface = { @@ -641,10 +641,10 @@ export class GameScene extends DirtyScene { }); this.connection.onServerDisconnected(() => { - console.log("Player disconnected from server. Reloading scene."); + console.log('Player disconnected from server. Reloading scene.'); this.cleanupClosingScene(); - const gameSceneKey = "somekey" + Math.round(Math.random() * 10000); + const gameSceneKey = 'somekey' + Math.round(Math.random() * 10000); const game: Phaser.Scene = new GameScene(this.room, this.MapUrlFile, gameSceneKey); this.scene.add(gameSceneKey, game, true, { initPosition: { @@ -723,34 +723,34 @@ export class GameScene extends DirtyScene { private initCirclesCanvas(): void { // Let's generate the circle for the group delimiter let circleElement = Object.values(this.textures.list).find( - (object: Texture) => object.key === "circleSprite-white" + (object: Texture) => object.key === 'circleSprite-white' ); if (circleElement) { - this.textures.remove("circleSprite-white"); + this.textures.remove('circleSprite-white'); } - circleElement = Object.values(this.textures.list).find((object: Texture) => object.key === "circleSprite-red"); + circleElement = Object.values(this.textures.list).find((object: Texture) => object.key === 'circleSprite-red'); if (circleElement) { - this.textures.remove("circleSprite-red"); + this.textures.remove('circleSprite-red'); } //create white circle canvas use to create sprite - this.circleTexture = this.textures.createCanvas("circleSprite-white", 96, 96); + this.circleTexture = this.textures.createCanvas('circleSprite-white', 96, 96); const context = this.circleTexture.context; context.beginPath(); context.arc(48, 48, 48, 0, 2 * Math.PI, false); // context.lineWidth = 5; - context.strokeStyle = "#ffffff"; + context.strokeStyle = '#ffffff'; context.stroke(); this.circleTexture.refresh(); //create red circle canvas use to create sprite - this.circleRedTexture = this.textures.createCanvas("circleSprite-red", 96, 96); + this.circleRedTexture = this.textures.createCanvas('circleSprite-red', 96, 96); const contextRed = this.circleRedTexture.context; contextRed.beginPath(); contextRed.arc(48, 48, 48, 0, 2 * Math.PI, false); //context.lineWidth = 5; - contextRed.strokeStyle = "#ff0000"; + contextRed.strokeStyle = '#ff0000'; contextRed.stroke(); this.circleRedTexture.refresh(); } @@ -765,35 +765,35 @@ export class GameScene extends DirtyScene { } private triggerOnMapLayerPropertyChange() { - this.gameMap.onPropertyChange("exitSceneUrl", (newValue, oldValue) => { + this.gameMap.onPropertyChange('exitSceneUrl', (newValue, oldValue) => { if (newValue) this.onMapExit(newValue as string); }); - this.gameMap.onPropertyChange("exitUrl", (newValue, oldValue) => { + this.gameMap.onPropertyChange('exitUrl', (newValue, oldValue) => { if (newValue) this.onMapExit(newValue as string); }); - this.gameMap.onPropertyChange("openWebsite", (newValue, oldValue, allProps) => { + this.gameMap.onPropertyChange('openWebsite', (newValue, oldValue, allProps) => { if (newValue === undefined) { - layoutManager.removeActionButton("openWebsite", this.userInputManager); + layoutManager.removeActionButton('openWebsite', this.userInputManager); 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('openWebsiteAllowApi') as boolean | undefined, + allProps.get('openWebsitePolicy') as string | undefined ); - layoutManager.removeActionButton("openWebsite", this.userInputManager); + layoutManager.removeActionButton('openWebsite', this.userInputManager); }; 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"; + message = 'Press SPACE or touch here to open web site'; } layoutManager.addActionButton( - "openWebsite", + 'openWebsite', message.toString(), () => { openWebsiteFunction(); @@ -805,32 +805,32 @@ export class GameScene extends DirtyScene { } } }); - this.gameMap.onPropertyChange("jitsiRoom", (newValue, oldValue, allProps) => { + this.gameMap.onPropertyChange('jitsiRoom', (newValue, oldValue, allProps) => { if (newValue === undefined) { - layoutManager.removeActionButton("jitsiRoom", this.userInputManager); + layoutManager.removeActionButton('jitsiRoom', this.userInputManager); this.stopJitsi(); } else { const openJitsiRoomFunction = () => { const roomName = jitsiFactory.getRoomName(newValue.toString(), this.instance); - const jitsiUrl = allProps.get("jitsiUrl") as string | undefined; + const jitsiUrl = allProps.get('jitsiUrl') as string | undefined; if (JITSI_PRIVATE_MODE && !jitsiUrl) { - const adminTag = allProps.get("jitsiRoomAdminTag") as string | undefined; + const adminTag = allProps.get('jitsiRoomAdminTag') as string | undefined; this.connection?.emitQueryJitsiJwtMessage(roomName, adminTag); } else { this.startJitsi(roomName, undefined); } - layoutManager.removeActionButton("jitsiRoom", this.userInputManager); + layoutManager.removeActionButton('jitsiRoom', this.userInputManager); }; const jitsiTriggerValue = allProps.get(TRIGGER_JITSI_PROPERTIES); if (jitsiTriggerValue && jitsiTriggerValue === ON_ACTION_TRIGGER_BUTTON) { let message = allProps.get(JITSI_MESSAGE_PROPERTIES); if (message === undefined) { - message = "Press SPACE or touch here to enter Jitsi Meet room"; + message = 'Press SPACE or touch here to enter Jitsi Meet room'; } layoutManager.addActionButton( - "jitsiRoom", + 'jitsiRoom', message.toString(), () => { openJitsiRoomFunction(); @@ -842,14 +842,14 @@ export class GameScene extends DirtyScene { } } }); - this.gameMap.onPropertyChange("silent", (newValue, oldValue) => { - if (newValue === undefined || newValue === false || newValue === "") { + this.gameMap.onPropertyChange('silent', (newValue, oldValue) => { + if (newValue === undefined || newValue === false || newValue === '') { this.connection?.setSilent(false); } else { this.connection?.setSilent(true); } }); - this.gameMap.onPropertyChange("playAudio", (newValue, oldValue, allProps) => { + this.gameMap.onPropertyChange('playAudio', (newValue, oldValue, allProps) => { const volume = allProps.get(AUDIO_VOLUME_PROPERTY) as number | undefined; const loop = allProps.get(AUDIO_LOOP_PROPERTY) as boolean | undefined; newValue === undefined @@ -857,14 +857,14 @@ export class GameScene extends DirtyScene { : audioManager.playAudio(newValue, this.getMapDirUrl(), volume, loop); }); // TODO: This legacy property should be removed at some point - this.gameMap.onPropertyChange("playAudioLoop", (newValue, oldValue) => { + this.gameMap.onPropertyChange('playAudioLoop', (newValue, oldValue) => { newValue === undefined ? audioManager.unloadAudio() : audioManager.playAudio(newValue, this.getMapDirUrl(), undefined, true); }); - this.gameMap.onPropertyChange("zone", (newValue, oldValue) => { - if (newValue === undefined || newValue === false || newValue === "") { + this.gameMap.onPropertyChange('zone', (newValue, oldValue) => { + if (newValue === undefined || newValue === false || newValue === '') { iframeListener.sendLeaveEvent(oldValue as string); } else { iframeListener.sendEnterEvent(newValue as string); @@ -892,22 +892,22 @@ export class GameScene extends DirtyScene { let html = `