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

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