Adding variables (on the front side for now)
This commit is contained in:
parent
1806ef9d7e
commit
ea1460abaf
@ -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...
|
||||||
|
@ -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;
|
||||||
|
10
front/src/Api/Events/InitEvent.ts
Normal file
10
front/src/Api/Events/InitEvent.ts
Normal 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>;
|
18
front/src/Api/Events/SetVariableEvent.ts
Normal file
18
front/src/Api/Events/SetVariableEvent.ts
Normal 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();
|
@ -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.
|
||||||
*/
|
*/
|
||||||
|
@ -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();
|
||||||
|
@ -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();
|
||||||
|
|
||||||
|
59
front/src/Phaser/Game/SharedVariablesManager.ts
Normal file
59
front/src/Phaser/Game/SharedVariablesManager.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
@ -206,3 +206,9 @@ window.addEventListener(
|
|||||||
// ...
|
// ...
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Notify WorkAdventure that we are ready to receive data
|
||||||
|
sendToWorkadventure({
|
||||||
|
type: 'ready',
|
||||||
|
data: null
|
||||||
|
});
|
||||||
|
11
maps/tests/Variables/script.js
Normal file
11
maps/tests/Variables/script.js
Normal 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'));
|
||||||
|
|
||||||
|
|
112
maps/tests/Variables/variables.json
Normal file
112
maps/tests/Variables/variables.json
Normal 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
|
||||||
|
}
|
@ -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>
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user