Adding variables (on the front side for now)

This commit is contained in:
David Négrier 2021-07-02 11:31:44 +02:00
parent 1806ef9d7e
commit ea1460abaf
13 changed files with 453 additions and 68 deletions

View file

@ -145,14 +145,15 @@ WA.room.setTiles([
]); ]);
``` ```
### Saving / loading metadata ### Saving / loading state
``` ```
WA.room.saveMetadata(key : string, data : any): void WA.room.saveVariable(key : string, data : unknown): void
WA.room.loadMetadata(key : string) : any 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. `data` can be any value that is serializable in JSON.
@ -161,17 +162,62 @@ configuration / metadatas.
Example : Example :
```javascript ```javascript
WA.room.saveMetadata('config', { WA.room.saveVariable('config', {
'bottomExitUrl': '/@/org/world/castle', 'bottomExitUrl': '/@/org/world/castle',
'topExitUrl': '/@/org/world/tower', 'topExitUrl': '/@/org/world/tower',
'enableBirdSound': true 'enableBirdSound': true
}); });
//... //...
let config = WA.room.loadMetadata('config'); let config = WA.room.loadVariable('config');
``` ```
{.alert.alert-danger} If you are using Typescript, please note that the return type of `loadVariable` is `unknown`. This is
Important: metadata can only be saved/loaded if an administration server is attached to WorkAdventure. The `WA.room.saveMetadata` for security purpose, as we don't know the type of the variable. In order to use the returned value,
and `WA.room.loadMetadata` functions will therefore be available on the hosted version of WorkAdventure, but will not 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
be available in the self-hosted version (unless you decide to code an administration server stub to provide storage for that you get the expected type).
those data)
{.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.
<div class="row">
<div class="col">
<img src="https://workadventu.re/img/docs/object_variable.png" class="figure-img img-fluid rounded" alt="" />
</div>
</div>
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...

View file

@ -18,6 +18,9 @@ import type { MenuItemClickedEvent } from "./ui/MenuItemClickedEvent";
import type { MenuItemRegisterEvent } from "./ui/MenuItemRegisterEvent"; import type { MenuItemRegisterEvent } from "./ui/MenuItemRegisterEvent";
import type { HasPlayerMovedEvent } from "./HasPlayerMovedEvent"; import type { HasPlayerMovedEvent } from "./HasPlayerMovedEvent";
import type { SetTilesEvent } from "./SetTilesEvent"; import type { SetTilesEvent } from "./SetTilesEvent";
import type { SetVariableEvent } from "./SetVariableEvent";
import type {InitEvent} from "./InitEvent";
export interface TypedMessageEvent<T> extends MessageEvent { export interface TypedMessageEvent<T> extends MessageEvent {
data: T; data: T;
@ -50,6 +53,9 @@ export type IframeEventMap = {
getState: undefined; getState: undefined;
registerMenuCommand: MenuItemRegisterEvent; registerMenuCommand: MenuItemRegisterEvent;
setTiles: SetTilesEvent; setTiles: SetTilesEvent;
setVariable: SetVariableEvent;
// A script/iframe is ready to receive events
ready: null;
}; };
export interface IframeEvent<T extends keyof IframeEventMap> { export interface IframeEvent<T extends keyof IframeEventMap> {
type: T; type: T;
@ -68,6 +74,8 @@ export interface IframeResponseEventMap {
hasPlayerMoved: HasPlayerMovedEvent; hasPlayerMoved: HasPlayerMovedEvent;
dataLayer: DataLayerEvent; dataLayer: DataLayerEvent;
menuItemClicked: MenuItemClickedEvent; menuItemClicked: MenuItemClickedEvent;
setVariable: SetVariableEvent;
init: InitEvent;
} }
export interface IframeResponseEvent<T extends keyof IframeResponseEventMap> { export interface IframeResponseEvent<T extends keyof IframeResponseEventMap> {
type: T; type: T;

View file

@ -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<typeof isInitEvent>;

View file

@ -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<typeof isSetVariableEvent>;
export const isSetVariableIframeEvent =
new tg.IsInterface().withProperties({
type: tg.isSingletonString("setVariable"),
data: isSetVariableEvent
}).get();

View file

@ -32,6 +32,7 @@ import type { HasPlayerMovedEvent } from "./Events/HasPlayerMovedEvent";
import { isLoadPageEvent } from "./Events/LoadPageEvent"; import { isLoadPageEvent } from "./Events/LoadPageEvent";
import { handleMenuItemRegistrationEvent, isMenuItemRegisterIframeEvent } from "./Events/ui/MenuItemRegisterEvent"; import { handleMenuItemRegistrationEvent, isMenuItemRegisterIframeEvent } from "./Events/ui/MenuItemRegisterEvent";
import { SetTilesEvent, isSetTilesEvent } from "./Events/SetTilesEvent"; import { SetTilesEvent, isSetTilesEvent } from "./Events/SetTilesEvent";
import { isSetVariableIframeEvent, SetVariableEvent } from "./Events/SetVariableEvent";
type AnswererCallback<T extends keyof IframeQueryMap> = (query: IframeQueryMap[T]['query']) => IframeQueryMap[T]['answer']|Promise<IframeQueryMap[T]['answer']>; type AnswererCallback<T extends keyof IframeQueryMap> = (query: IframeQueryMap[T]['query']) => IframeQueryMap[T]['answer']|Promise<IframeQueryMap[T]['answer']>;
@ -40,6 +41,9 @@ type AnswererCallback<T extends keyof IframeQueryMap> = (query: IframeQueryMap[T
* Also allows to send messages to those iframes. * Also allows to send messages to those iframes.
*/ */
class IframeListener { class IframeListener {
private readonly _readyStream: Subject<HTMLIFrameElement> = new Subject();
public readonly readyStream = this._readyStream.asObservable();
private readonly _chatStream: Subject<ChatEvent> = new Subject(); private readonly _chatStream: Subject<ChatEvent> = new Subject();
public readonly chatStream = this._chatStream.asObservable(); public readonly chatStream = this._chatStream.asObservable();
@ -106,6 +110,9 @@ class IframeListener {
private readonly _setTilesStream: Subject<SetTilesEvent> = new Subject(); private readonly _setTilesStream: Subject<SetTilesEvent> = new Subject();
public readonly setTilesStream = this._setTilesStream.asObservable(); public readonly setTilesStream = this._setTilesStream.asObservable();
private readonly _setVariableStream: Subject<SetVariableEvent> = new Subject();
public readonly setVariableStream = this._setVariableStream.asObservable();
private readonly iframes = new Set<HTMLIFrameElement>(); private readonly iframes = new Set<HTMLIFrameElement>();
private readonly iframeCloseCallbacks = new Map<HTMLIFrameElement, (() => void)[]>(); private readonly iframeCloseCallbacks = new Map<HTMLIFrameElement, (() => void)[]>();
private readonly scripts = new Map<string, HTMLIFrameElement>(); private readonly scripts = new Map<string, HTMLIFrameElement>();
@ -187,62 +194,76 @@ class IframeListener {
}); });
} else if (isIframeEventWrapper(payload)) { } else if (isIframeEventWrapper(payload)) {
if (payload.type === "showLayer" && isLayerEvent(payload.data)) { if (payload.type === 'ready') {
this._showLayerStream.next(payload.data); this._readyStream.next();
} else if (payload.type === "hideLayer" && isLayerEvent(payload.data)) { } else if (payload.type === "showLayer" && isLayerEvent(payload.data)) {
this._hideLayerStream.next(payload.data); this._showLayerStream.next(payload.data);
} else if (payload.type === "setProperty" && isSetPropertyEvent(payload.data)) { } else if (payload.type === "hideLayer" && isLayerEvent(payload.data)) {
this._setPropertyStream.next(payload.data); this._hideLayerStream.next(payload.data);
} else if (payload.type === "chat" && isChatEvent(payload.data)) { } else if (payload.type === "setProperty" && isSetPropertyEvent(payload.data)) {
this._chatStream.next(payload.data); this._setPropertyStream.next(payload.data);
} else if (payload.type === "openPopup" && isOpenPopupEvent(payload.data)) { } else if (payload.type === "chat" && isChatEvent(payload.data)) {
this._openPopupStream.next(payload.data); this._chatStream.next(payload.data);
} else if (payload.type === "closePopup" && isClosePopupEvent(payload.data)) { } else if (payload.type === "openPopup" && isOpenPopupEvent(payload.data)) {
this._closePopupStream.next(payload.data); this._openPopupStream.next(payload.data);
} else if (payload.type === "openTab" && isOpenTabEvent(payload.data)) { } else if (payload.type === "closePopup" && isClosePopupEvent(payload.data)) {
scriptUtils.openTab(payload.data.url); this._closePopupStream.next(payload.data);
} else if (payload.type === "goToPage" && isGoToPageEvent(payload.data)) { } else if (payload.type === "openTab" && isOpenTabEvent(payload.data)) {
scriptUtils.goToPage(payload.data.url); scriptUtils.openTab(payload.data.url);
} else if (payload.type === "loadPage" && isLoadPageEvent(payload.data)) { } else if (payload.type === "goToPage" && isGoToPageEvent(payload.data)) {
this._loadPageStream.next(payload.data.url); scriptUtils.goToPage(payload.data.url);
} else if (payload.type === "playSound" && isPlaySoundEvent(payload.data)) { } else if (payload.type === "loadPage" && isLoadPageEvent(payload.data)) {
this._playSoundStream.next(payload.data); this._loadPageStream.next(payload.data.url);
} else if (payload.type === "stopSound" && isStopSoundEvent(payload.data)) { } else if (payload.type === "playSound" && isPlaySoundEvent(payload.data)) {
this._stopSoundStream.next(payload.data); this._playSoundStream.next(payload.data);
} else if (payload.type === "loadSound" && isLoadSoundEvent(payload.data)) { } else if (payload.type === "stopSound" && isStopSoundEvent(payload.data)) {
this._loadSoundStream.next(payload.data); this._stopSoundStream.next(payload.data);
} else if (payload.type === "openCoWebSite" && isOpenCoWebsite(payload.data)) { } else if (payload.type === "loadSound" && isLoadSoundEvent(payload.data)) {
scriptUtils.openCoWebsite( this._loadSoundStream.next(payload.data);
payload.data.url, } else if (payload.type === "openCoWebSite" && isOpenCoWebsite(payload.data)) {
foundSrc, scriptUtils.openCoWebsite(
payload.data.allowApi, payload.data.url,
payload.data.allowPolicy foundSrc,
); payload.data.allowApi,
} else if (payload.type === "closeCoWebSite") { payload.data.allowPolicy
scriptUtils.closeCoWebSite(); );
} else if (payload.type === "disablePlayerControls") { } else if (payload.type === "closeCoWebSite") {
this._disablePlayerControlStream.next(); scriptUtils.closeCoWebSite();
} else if (payload.type === "restorePlayerControls") { } else if (payload.type === "disablePlayerControls") {
this._enablePlayerControlStream.next(); this._disablePlayerControlStream.next();
} else if (payload.type === "displayBubble") { } else if (payload.type === "restorePlayerControls") {
this._displayBubbleStream.next(); this._enablePlayerControlStream.next();
} else if (payload.type === "removeBubble") { } else if (payload.type === "displayBubble") {
this._removeBubbleStream.next(); this._displayBubbleStream.next();
} else if (payload.type == "onPlayerMove") { } else if (payload.type === "removeBubble") {
this.sendPlayerMove = true; this._removeBubbleStream.next();
} else if (payload.type == "getDataLayer") { } else if (payload.type == "onPlayerMove") {
this._dataLayerChangeStream.next(); this.sendPlayerMove = true;
} else if (isMenuItemRegisterIframeEvent(payload)) { } else if (payload.type == "getDataLayer") {
const data = payload.data.menutItem; this._dataLayerChangeStream.next();
// @ts-ignore } else if (isMenuItemRegisterIframeEvent(payload)) {
this.iframeCloseCallbacks.get(iframe).push(() => { const data = payload.data.menutItem;
this._unregisterMenuCommandStream.next(data); // @ts-ignore
}); this.iframeCloseCallbacks.get(iframe).push(() => {
handleMenuItemRegistrationEvent(payload.data); this._unregisterMenuCommandStream.next(data);
} else if (payload.type == "setTiles" && isSetTilesEvent(payload.data)) { });
this._setTilesStream.next(payload.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 false
); );
@ -394,6 +415,13 @@ class IframeListener {
}); });
} }
setVariable(setVariableEvent: SetVariableEvent) {
this.postMessage({
'type': 'setVariable',
'data': setVariableEvent
});
}
/** /**
* Sends the message... to all allowed iframes. * Sends the message... to all allowed iframes.
*/ */

View file

@ -1,4 +1,4 @@
import { Subject } from "rxjs"; import {Observable, Subject} from "rxjs";
import { isDataLayerEvent } from "../Events/DataLayerEvent"; import { isDataLayerEvent } from "../Events/DataLayerEvent";
import { EnterLeaveEvent, isEnterLeaveEvent } from "../Events/EnterLeaveEvent"; import { EnterLeaveEvent, isEnterLeaveEvent } from "../Events/EnterLeaveEvent";
@ -6,6 +6,9 @@ import { isGameStateEvent } from "../Events/GameStateEvent";
import {IframeApiContribution, queryWorkadventure, sendToWorkadventure} from "./IframeApiContribution"; import {IframeApiContribution, queryWorkadventure, sendToWorkadventure} from "./IframeApiContribution";
import { apiCallback } from "./registeredCallbacks"; 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 { ITiledMap } from "../../Phaser/Map/ITiledMap";
import type { DataLayerEvent } from "../Events/DataLayerEvent"; import type { DataLayerEvent } from "../Events/DataLayerEvent";
@ -15,6 +18,9 @@ const enterStreams: Map<string, Subject<EnterLeaveEvent>> = new Map<string, Subj
const leaveStreams: Map<string, Subject<EnterLeaveEvent>> = new Map<string, Subject<EnterLeaveEvent>>(); const leaveStreams: Map<string, Subject<EnterLeaveEvent>> = new Map<string, Subject<EnterLeaveEvent>>();
const dataLayerResolver = new Subject<DataLayerEvent>(); const dataLayerResolver = new Subject<DataLayerEvent>();
const stateResolvers = new Subject<GameStateEvent>(); const stateResolvers = new Subject<GameStateEvent>();
const setVariableResolvers = new Subject<SetVariableEvent>();
const variables = new Map<string, unknown>();
const variableSubscribers = new Map<string, Subject<unknown>>();
let immutableDataPromise: Promise<GameStateEvent> | undefined = undefined; let immutableDataPromise: Promise<GameStateEvent> | undefined = undefined;
@ -52,6 +58,14 @@ function getDataLayer(): Promise<DataLayerEvent> {
}); });
} }
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<WorkadventureRoomCommands> { export class WorkadventureRoomCommands extends IframeApiContribution<WorkadventureRoomCommands> {
callbacks = [ callbacks = [
apiCallback({ apiCallback({
@ -75,6 +89,13 @@ export class WorkadventureRoomCommands extends IframeApiContribution<Workadventu
dataLayerResolver.next(payloadData); dataLayerResolver.next(payloadData);
}, },
}), }),
apiCallback({
type: "setVariable",
typeChecker: isSetVariableEvent,
callback: (payloadData) => {
setVariableResolvers.next(payloadData);
}
}),
]; ];
onEnterZone(name: string, callback: () => void): void { onEnterZone(name: string, callback: () => void): void {
@ -132,6 +153,30 @@ export class WorkadventureRoomCommands extends IframeApiContribution<Workadventu
data: tiles, data: tiles,
}); });
} }
saveVariable(key : string, value : unknown): void {
variables.set(key, value);
sendToWorkadventure({
type: 'setVariable',
data: {
key,
value
}
})
}
loadVariable(key: string): unknown {
return variables.get(key);
}
onVariableChange(key: string): Observable<unknown> {
let subject = variableSubscribers.get(key);
if (subject === undefined) {
subject = new Subject<unknown>();
variableSubscribers.set(key, subject);
}
return subject.asObservable();
}
} }
export default new WorkadventureRoomCommands(); export default new WorkadventureRoomCommands();

View file

@ -91,6 +91,8 @@ import { soundManager } from "./SoundManager";
import { peerStore, screenSharingPeerStore } from "../../Stores/PeerStore"; import { peerStore, screenSharingPeerStore } from "../../Stores/PeerStore";
import { videoFocusStore } from "../../Stores/VideoFocusStore"; import { videoFocusStore } from "../../Stores/VideoFocusStore";
import { biggestAvailableAreaStore } from "../../Stores/BiggestAvailableAreaStore"; import { biggestAvailableAreaStore } from "../../Stores/BiggestAvailableAreaStore";
import { SharedVariablesManager } from "./SharedVariablesManager";
import type {InitEvent} from "../../Api/Events/InitEvent";
export interface GameSceneInitInterface { export interface GameSceneInitInterface {
initPosition: PointInterface | null; 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 mapTransitioning: boolean = false; //used to prevent transitions happenning at the same time.
private emoteManager!: EmoteManager; private emoteManager!: EmoteManager;
private preloading: boolean = true; private preloading: boolean = true;
startPositionCalculator!: StartPositionCalculator; private startPositionCalculator!: StartPositionCalculator;
private sharedVariablesManager!: SharedVariablesManager;
constructor(private room: Room, MapUrlFile: string, customKey?: string | undefined) { constructor(private room: Room, MapUrlFile: string, customKey?: string | undefined) {
super({ 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 // Now, let's load the script, if any
const scripts = this.getScriptUrls(this.mapFile); const scripts = this.getScriptUrls(this.mapFile);
for (const script of scripts) { for (const script of scripts) {
@ -706,6 +726,9 @@ export class GameScene extends DirtyScene {
this.gameMap.setPosition(event.x, event.y); this.gameMap.setPosition(event.x, event.y);
}); });
// Set up variables manager
this.sharedVariablesManager = new SharedVariablesManager(this.connection, this.gameMap);
//this.initUsersPosition(roomJoinedMessage.users); //this.initUsersPosition(roomJoinedMessage.users);
this.connectionAnswerPromiseResolve(onConnect.room); this.connectionAnswerPromiseResolve(onConnect.room);
// Analyze tags to find if we are admin. If yes, show console. // Analyze tags to find if we are admin. If yes, show console.
@ -1148,6 +1171,7 @@ ${escapedMessage}
this.peerStoreUnsubscribe(); this.peerStoreUnsubscribe();
this.biggestAvailableAreaStoreUnsubscribe(); this.biggestAvailableAreaStoreUnsubscribe();
iframeListener.unregisterAnswerer('getState'); iframeListener.unregisterAnswerer('getState');
this.sharedVariablesManager?.close();
mediaManager.hideGameOverlay(); mediaManager.hideGameOverlay();

View file

@ -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<string, unknown>();
private iframeListenerSubscription: Subscription;
private variableObjects: Map<string, ITiledMapObject>;
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<string, ITiledMapObject> {
const objects = new Map<string, ITiledMapObject>();
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<string, unknown> {
return this._variables;
}
}

View file

@ -206,3 +206,9 @@ window.addEventListener(
// ... // ...
} }
); );
// Notify WorkAdventure that we are ready to receive data
sendToWorkadventure({
type: 'ready',
data: null
});

View file

@ -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'));

View file

@ -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
}

View file

@ -202,6 +202,14 @@
<a href="#" class="testLink" data-testmap="Metadata/setTiles.json" target="_blank">Test set tiles</a> <a href="#" class="testLink" data-testmap="Metadata/setTiles.json" target="_blank">Test set tiles</a>
</td> </td>
</tr> </tr>
<tr>
<td>
<input type="radio" name="test-variables"> Success <input type="radio" name="test-variables"> Failure <input type="radio" name="test-variables" checked> Pending
</td>
<td>
<a href="#" class="testLink" data-testmap="Variables/variables.json" target="_blank">Testing scripting variables</a>
</td>
</tr>
</table> </table>
<script> <script>

View file

@ -94,6 +94,7 @@ message ClientToServerMessage {
ReportPlayerMessage reportPlayerMessage = 11; ReportPlayerMessage reportPlayerMessage = 11;
QueryJitsiJwtMessage queryJitsiJwtMessage = 12; QueryJitsiJwtMessage queryJitsiJwtMessage = 12;
EmotePromptMessage emotePromptMessage = 13; EmotePromptMessage emotePromptMessage = 13;
VariableMessage variableMessage = 14;
} }
} }
@ -107,6 +108,11 @@ message ItemEventMessage {
string parametersJson = 4; string parametersJson = 4;
} }
message VariableMessage {
string name = 1;
string value = 2;
}
message PlayGlobalMessage { message PlayGlobalMessage {
string id = 1; string id = 1;
string type = 2; string type = 2;
@ -133,6 +139,7 @@ message SubMessage {
UserLeftMessage userLeftMessage = 5; UserLeftMessage userLeftMessage = 5;
ItemEventMessage itemEventMessage = 6; ItemEventMessage itemEventMessage = 6;
EmoteEventMessage emoteEventMessage = 7; EmoteEventMessage emoteEventMessage = 7;
VariableMessage variableMessage = 8;
} }
} }
@ -179,6 +186,7 @@ message RoomJoinedMessage {
repeated ItemStateMessage item = 3; repeated ItemStateMessage item = 3;
int32 currentUserId = 4; int32 currentUserId = 4;
repeated string tag = 5; repeated string tag = 5;
repeated VariableMessage = 6;
} }
message WebRtcStartMessage { message WebRtcStartMessage {
@ -257,7 +265,7 @@ message ServerToClientMessage {
AdminRoomMessage adminRoomMessage = 14; AdminRoomMessage adminRoomMessage = 14;
WorldFullWarningMessage worldFullWarningMessage = 15; WorldFullWarningMessage worldFullWarningMessage = 15;
WorldFullMessage worldFullMessage = 16; WorldFullMessage worldFullMessage = 16;
RefreshRoomMessage refreshRoomMessage = 17; RefreshRoomMessage refreshRoomMessage = 17;
WorldConnexionMessage worldConnexionMessage = 18; WorldConnexionMessage worldConnexionMessage = 18;
EmoteEventMessage emoteEventMessage = 19; EmoteEventMessage emoteEventMessage = 19;
} }
@ -333,6 +341,7 @@ message PusherToBackMessage {
SendUserMessage sendUserMessage = 12; SendUserMessage sendUserMessage = 12;
BanUserMessage banUserMessage = 13; BanUserMessage banUserMessage = 13;
EmotePromptMessage emotePromptMessage = 14; EmotePromptMessage emotePromptMessage = 14;
VariableMessage variableMessage = 15;
} }
} }
@ -351,6 +360,7 @@ message SubToPusherMessage {
SendUserMessage sendUserMessage = 7; SendUserMessage sendUserMessage = 7;
BanUserMessage banUserMessage = 8; BanUserMessage banUserMessage = 8;
EmoteEventMessage emoteEventMessage = 9; EmoteEventMessage emoteEventMessage = 9;
VariableMessage variableMessage = 10;
} }
} }