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.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.
<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 { HasPlayerMovedEvent } from "./HasPlayerMovedEvent";
import type { SetTilesEvent } from "./SetTilesEvent";
import type { SetVariableEvent } from "./SetVariableEvent";
import type {InitEvent} from "./InitEvent";
export interface TypedMessageEvent<T> 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<T extends keyof IframeEventMap> {
type: T;
@ -68,6 +74,8 @@ export interface IframeResponseEventMap {
hasPlayerMoved: HasPlayerMovedEvent;
dataLayer: DataLayerEvent;
menuItemClicked: MenuItemClickedEvent;
setVariable: SetVariableEvent;
init: InitEvent;
}
export interface IframeResponseEvent<T extends keyof IframeResponseEventMap> {
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 { handleMenuItemRegistrationEvent, isMenuItemRegisterIframeEvent } from "./Events/ui/MenuItemRegisterEvent";
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']>;
@ -40,6 +41,9 @@ type AnswererCallback<T extends keyof IframeQueryMap> = (query: IframeQueryMap[T
* Also allows to send messages to those iframes.
*/
class IframeListener {
private readonly _readyStream: Subject<HTMLIFrameElement> = new Subject();
public readonly readyStream = this._readyStream.asObservable();
private readonly _chatStream: Subject<ChatEvent> = new Subject();
public readonly chatStream = this._chatStream.asObservable();
@ -106,6 +110,9 @@ class IframeListener {
private readonly _setTilesStream: Subject<SetTilesEvent> = new Subject();
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 iframeCloseCallbacks = new Map<HTMLIFrameElement, (() => void)[]>();
private readonly scripts = new Map<string, HTMLIFrameElement>();
@ -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.
*/

View file

@ -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<string, Subject<EnterLeaveEvent>> = new Map<string, Subj
const leaveStreams: Map<string, Subject<EnterLeaveEvent>> = new Map<string, Subject<EnterLeaveEvent>>();
const dataLayerResolver = new Subject<DataLayerEvent>();
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;
@ -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> {
callbacks = [
apiCallback({
@ -75,6 +89,13 @@ export class WorkadventureRoomCommands extends IframeApiContribution<Workadventu
dataLayerResolver.next(payloadData);
},
}),
apiCallback({
type: "setVariable",
typeChecker: isSetVariableEvent,
callback: (payloadData) => {
setVariableResolvers.next(payloadData);
}
}),
];
onEnterZone(name: string, callback: () => void): void {
@ -132,6 +153,30 @@ export class WorkadventureRoomCommands extends IframeApiContribution<Workadventu
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();

View file

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

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>
</td>
</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>
<script>

View file

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