Merge remote-tracking branch 'remotes/upstream/develop' into trigger-message-refv3

This commit is contained in:
jonny 2021-07-02 18:49:22 +02:00
commit 369d453455
202 changed files with 15971 additions and 4927 deletions

View file

@ -1,13 +1,12 @@
import * as tg from "generic-type-guard";
export const isDataLayerEvent =
new tg.IsInterface().withProperties({
data: tg.isObject
}).get();
export const isDataLayerEvent = new tg.IsInterface()
.withProperties({
data: tg.isObject,
})
.get();
/**
* A message sent from the game to the iFrame when the data of the layers change after the iFrame send a message to the game that it want to listen to the data of the layers
*/
export type DataLayerEvent = tg.GuardedType<typeof isDataLayerEvent>;
export type DataLayerEvent = tg.GuardedType<typeof isDataLayerEvent>;

View file

@ -1,14 +1,15 @@
import * as tg from "generic-type-guard";
export const isGameStateEvent =
new tg.IsInterface().withProperties({
roomId: tg.isString,
mapUrl: tg.isString,
nickname: tg.isUnion(tg.isString, tg.isNull),
uuid: tg.isUnion(tg.isString, tg.isUndefined),
startLayerName: tg.isUnion(tg.isString, tg.isNull),
tags : tg.isArray(tg.isString),
}).get();
export const isGameStateEvent = new tg.IsInterface()
.withProperties({
roomId: tg.isString,
mapUrl: tg.isString,
nickname: tg.isUnion(tg.isString, tg.isNull),
uuid: tg.isUnion(tg.isString, tg.isUndefined),
startLayerName: tg.isUnion(tg.isString, tg.isNull),
tags: tg.isArray(tg.isString),
})
.get();
/**
* A message sent from the game to the iFrame when the gameState is received by the script
*/

View file

@ -1,19 +1,17 @@
import * as tg from "generic-type-guard";
export const isHasPlayerMovedEvent =
new tg.IsInterface().withProperties({
direction: tg.isElementOf('right', 'left', 'up', 'down'),
export const isHasPlayerMovedEvent = new tg.IsInterface()
.withProperties({
direction: tg.isElementOf("right", "left", "up", "down"),
moving: tg.isBoolean,
x: tg.isNumber,
y: tg.isNumber
}).get();
y: tg.isNumber,
})
.get();
/**
* A message sent from the game to the iFrame to notify a movement from the current player.
*/
export type HasPlayerMovedEvent = tg.GuardedType<typeof isHasPlayerMovedEvent>;
export type HasPlayerMovedEventCallback = (event: HasPlayerMovedEvent) => void
export type HasPlayerMovedEventCallback = (event: HasPlayerMovedEvent) => void;

View file

@ -1,78 +1,83 @@
import type { GameStateEvent } from './GameStateEvent';
import type { ButtonClickedEvent } from './ButtonClickedEvent';
import type { ChatEvent } from './ChatEvent';
import type { ClosePopupEvent } from './ClosePopupEvent';
import type { EnterLeaveEvent } from './EnterLeaveEvent';
import type { GoToPageEvent } from './GoToPageEvent';
import type { LoadPageEvent } from './LoadPageEvent';
import type { OpenCoWebSiteEvent } from './OpenCoWebSiteEvent';
import type { OpenPopupEvent } from './OpenPopupEvent';
import type { OpenTabEvent } from './OpenTabEvent';
import type { UserInputChatEvent } from './UserInputChatEvent';
import type { GameStateEvent } from "./GameStateEvent";
import type { ButtonClickedEvent } from "./ButtonClickedEvent";
import type { ChatEvent } from "./ChatEvent";
import type { ClosePopupEvent } from "./ClosePopupEvent";
import type { EnterLeaveEvent } from "./EnterLeaveEvent";
import type { GoToPageEvent } from "./GoToPageEvent";
import type { LoadPageEvent } from "./LoadPageEvent";
import type { OpenCoWebSiteEvent } from "./OpenCoWebSiteEvent";
import type { OpenPopupEvent } from "./OpenPopupEvent";
import type { OpenTabEvent } from "./OpenTabEvent";
import type { UserInputChatEvent } from "./UserInputChatEvent";
import type { DataLayerEvent } from "./DataLayerEvent";
import type { LayerEvent } from './LayerEvent';
import type { LayerEvent } from "./LayerEvent";
import type { SetPropertyEvent } from "./setPropertyEvent";
import type { LoadSoundEvent } from "./LoadSoundEvent";
import type { PlaySoundEvent } from "./PlaySoundEvent";
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 { MessageReferenceEvent, TriggerMessageEvent } from '../iframe/TriggerMessageEvent';
import type { SetTilesEvent } from "./SetTilesEvent";
import type {
MessageReferenceEvent,
removeTriggerMessage,
triggerMessage,
TriggerMessageEvent,
} from "./ui/TriggerMessageEvent";
export interface TypedMessageEvent<T> extends MessageEvent {
data: T
data: T;
}
/**
* List event types sent from an iFrame to WorkAdventure
*/
export type IframeEventMap = {
//getState: GameStateEvent,
// updateTile: UpdateTileEvent
loadPage: LoadPageEvent
chat: ChatEvent,
openPopup: OpenPopupEvent
closePopup: ClosePopupEvent
openTab: OpenTabEvent
goToPage: GoToPageEvent
openCoWebSite: OpenCoWebSiteEvent
closeCoWebSite: null
disablePlayerControls: null
restorePlayerControls: null
displayBubble: null
removeBubble: null
onPlayerMove: undefined
showLayer: LayerEvent
hideLayer: LayerEvent
setProperty: SetPropertyEvent
getDataLayer: undefined
loadSound: LoadSoundEvent
playSound: PlaySoundEvent
stopSound: null,
getState: undefined,
registerMenuCommand: MenuItemRegisterEvent
loadPage: LoadPageEvent;
chat: ChatEvent;
openPopup: OpenPopupEvent;
closePopup: ClosePopupEvent;
openTab: OpenTabEvent;
goToPage: GoToPageEvent;
openCoWebSite: OpenCoWebSiteEvent;
closeCoWebSite: null;
disablePlayerControls: null;
restorePlayerControls: null;
displayBubble: null;
removeBubble: null;
onPlayerMove: undefined;
showLayer: LayerEvent;
hideLayer: LayerEvent;
setProperty: SetPropertyEvent;
getDataLayer: undefined;
loadSound: LoadSoundEvent;
playSound: PlaySoundEvent;
stopSound: null;
getState: undefined;
registerMenuCommand: MenuItemRegisterEvent;
setTiles: SetTilesEvent;
triggerMessage: TriggerMessageEvent
removeTriggerMessage: MessageReferenceEvent
}
triggerMessage: TriggerMessageEvent;
removeTriggerMessage: MessageReferenceEvent;
};
export interface IframeEvent<T extends keyof IframeEventMap> {
type: T;
data: IframeEventMap[T];
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const isIframeEventWrapper = (event: any): event is IframeEvent<keyof IframeEventMap> => typeof event.type === 'string';
export const isIframeEventWrapper = (event: any): event is IframeEvent<keyof IframeEventMap> =>
typeof event.type === "string";
export interface IframeResponseEventMap {
userInputChat: UserInputChatEvent
enterEvent: EnterLeaveEvent
leaveEvent: EnterLeaveEvent
buttonClickedEvent: ButtonClickedEvent
gameState: GameStateEvent
hasPlayerMoved: HasPlayerMovedEvent
dataLayer: DataLayerEvent
menuItemClicked: MenuItemClickedEvent
messageTriggered: MessageReferenceEvent
userInputChat: UserInputChatEvent;
enterEvent: EnterLeaveEvent;
leaveEvent: EnterLeaveEvent;
buttonClickedEvent: ButtonClickedEvent;
hasPlayerMoved: HasPlayerMovedEvent;
dataLayer: DataLayerEvent;
menuItemClicked: MenuItemClickedEvent;
messageTriggered: MessageReferenceEvent;
}
export interface IframeResponseEvent<T extends keyof IframeResponseEventMap> {
type: T;
@ -80,4 +85,67 @@ export interface IframeResponseEvent<T extends keyof IframeResponseEventMap> {
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const isIframeResponseEventWrapper = (event: { type?: string }): event is IframeResponseEvent<keyof IframeResponseEventMap> => typeof event.type === 'string';
export const isIframeResponseEventWrapper = (event: {
type?: string;
}): event is IframeResponseEvent<keyof IframeResponseEventMap> => typeof event.type === "string";
/**
* List event types sent from an iFrame to WorkAdventure that expect a unique answer from WorkAdventure along the type for the answer from WorkAdventure to the iFrame
*/
export type IframeQueryMap = {
getState: {
query: undefined;
answer: GameStateEvent;
};
[triggerMessage]: {
query: TriggerMessageEvent;
answer: void;
};
[removeTriggerMessage]: {
query: MessageReferenceEvent;
answer: void;
};
};
export interface IframeQuery<T extends keyof IframeQueryMap> {
type: T;
data: IframeQueryMap[T]["query"];
}
export interface IframeQueryWrapper<T extends keyof IframeQueryMap> {
id: number;
query: IframeQuery<T>;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const isIframeQuery = (event: any): event is IframeQuery<keyof IframeQueryMap> => typeof event.type === "string";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const isIframeQueryWrapper = (event: any): event is IframeQueryWrapper<keyof IframeQueryMap> =>
typeof event.id === "number" && isIframeQuery(event.query);
export interface IframeAnswerEvent<T extends keyof IframeQueryMap> {
id: number;
type: T;
data: IframeQueryMap[T]["answer"];
}
export const isIframeAnswerEvent = (event: {
type?: string;
id?: number;
}): event is IframeAnswerEvent<keyof IframeQueryMap> => typeof event.type === "string" && typeof event.id === "number";
export interface IframeErrorAnswerEvent {
id: number;
type: keyof IframeQueryMap;
error: string;
}
export const isIframeErrorAnswerEvent = (event: {
type?: string;
id?: number;
error?: string;
}): event is IframeErrorAnswerEvent =>
typeof event.type === "string" && typeof event.id === "number" && typeof event.error === "string";

View file

@ -1,9 +1,10 @@
import * as tg from "generic-type-guard";
export const isLayerEvent =
new tg.IsInterface().withProperties({
export const isLayerEvent = new tg.IsInterface()
.withProperties({
name: tg.isString,
}).get();
})
.get();
/**
* A message sent from the iFrame to the game to show/hide a layer.
*/

View file

@ -1,13 +1,12 @@
import * as tg from "generic-type-guard";
export const isLoadPageEvent =
new tg.IsInterface().withProperties({
export const isLoadPageEvent = new tg.IsInterface()
.withProperties({
url: tg.isString,
}).get();
})
.get();
/**
* A message sent from the iFrame to the game to add a message in the chat.
*/
export type LoadPageEvent = tg.GuardedType<typeof isLoadPageEvent>;
export type LoadPageEvent = tg.GuardedType<typeof isLoadPageEvent>;

View file

@ -1,11 +1,12 @@
import * as tg from "generic-type-guard";
export const isOpenCoWebsite =
new tg.IsInterface().withProperties({
export const isOpenCoWebsite = new tg.IsInterface()
.withProperties({
url: tg.isString,
}).get();
allowApi: tg.isBoolean,
allowPolicy: tg.isString,
})
.get();
/**
* A message sent from the iFrame to the game to add a message in the chat.

View file

@ -0,0 +1,16 @@
import * as tg from "generic-type-guard";
export const isSetTilesEvent = tg.isArray(
new tg.IsInterface()
.withProperties({
x: tg.isNumber,
y: tg.isNumber,
tile: tg.isUnion(tg.isNumber, tg.isString),
layer: tg.isString,
})
.get()
);
/**
* A message sent from the iFrame to the game to set one or many tiles.
*/
export type SetTilesEvent = tg.GuardedType<typeof isSetTilesEvent>;

View file

@ -1,12 +1,13 @@
import * as tg from "generic-type-guard";
export const isSetPropertyEvent =
new tg.IsInterface().withProperties({
export const isSetPropertyEvent = new tg.IsInterface()
.withProperties({
layerName: tg.isString,
propertyName: tg.isString,
propertyValue: tg.isUnion(tg.isString, tg.isUnion(tg.isNumber, tg.isUnion(tg.isBoolean, tg.isUndefined)))
}).get();
propertyValue: tg.isUnion(tg.isString, tg.isUnion(tg.isNumber, tg.isUnion(tg.isBoolean, tg.isUndefined))),
})
.get();
/**
* A message sent from the iFrame to the game to change the value of the property of the layer
*/
export type SetPropertyEvent = tg.GuardedType<typeof isSetPropertyEvent>;
export type SetPropertyEvent = tg.GuardedType<typeof isSetPropertyEvent>;

View file

@ -1,12 +1,11 @@
import * as tg from "generic-type-guard";
export const isMenuItemClickedEvent =
new tg.IsInterface().withProperties({
menuItem: tg.isString
}).get();
export const isMenuItemClickedEvent = new tg.IsInterface()
.withProperties({
menuItem: tg.isString,
})
.get();
/**
* A message sent from the game to the iFrame when a menu item is clicked.
*/
export type MenuItemClickedEvent = tg.GuardedType<typeof isMenuItemClickedEvent>;

View file

@ -1,25 +1,26 @@
import * as tg from "generic-type-guard";
import { Subject } from 'rxjs';
import { Subject } from "rxjs";
export const isMenuItemRegisterEvent =
new tg.IsInterface().withProperties({
menutItem: tg.isString
}).get();
export const isMenuItemRegisterEvent = new tg.IsInterface()
.withProperties({
menutItem: tg.isString,
})
.get();
/**
* A message sent from the iFrame to the game to add a new menu item.
*/
export type MenuItemRegisterEvent = tg.GuardedType<typeof isMenuItemRegisterEvent>;
export const isMenuItemRegisterIframeEvent =
new tg.IsInterface().withProperties({
export const isMenuItemRegisterIframeEvent = new tg.IsInterface()
.withProperties({
type: tg.isSingletonString("registerMenuCommand"),
data: isMenuItemRegisterEvent
}).get();
data: isMenuItemRegisterEvent,
})
.get();
const _registerMenuCommandStream: Subject<string> = new Subject();
export const registerMenuCommandStream = _registerMenuCommandStream.asObservable();
export function handleMenuItemRegistrationEvent(event: MenuItemRegisterEvent) {
_registerMenuCommandStream.next(event.menutItem)
}
_registerMenuCommandStream.next(event.menutItem);
}

View file

@ -1,42 +1,35 @@
import { Subject } from 'rxjs';
import { iframeListener } from '../../IframeListener';
import { isMessageReferenceEvent, isTriggerMessageEvent, MessageReferenceEvent, removeTriggerMessage, triggerMessage, TriggerMessageEvent } from './TriggerMessageEvent';
import { Subject } from "rxjs";
import { iframeListener } from "../../IframeListener";
import {
isMessageReferenceEvent,
isTriggerMessageEvent,
MessageReferenceEvent,
removeTriggerMessage,
triggerMessage,
TriggerMessageEvent,
} from "./TriggerMessageEvent";
import * as tg from "generic-type-guard";
export function sendMessageTriggeredEvent(uuid: string) {
iframeListener.postMessage({
'type': 'messageTriggered',
'data': {
type: "messageTriggered",
data: {
uuid,
} as MessageReferenceEvent
} as MessageReferenceEvent,
});
}
const _triggerMessageEvent: Subject<TriggerMessageEvent> = new Subject();
const _removeTriggerMessageEvent: Subject<MessageReferenceEvent> = new Subject();
const isTriggerMessageEventObject = new tg.IsInterface()
.withProperties({
type: tg.isSingletonString(triggerMessage),
data: isTriggerMessageEvent,
})
.get();
export const triggerMessageEvent = _triggerMessageEvent.asObservable();
const isTriggerMessageRemoveEventObject = new tg.IsInterface()
.withProperties({
type: tg.isSingletonString(removeTriggerMessage),
data: isMessageReferenceEvent,
})
.get();
export const removeTriggerMessageEvent = _removeTriggerMessageEvent.asObservable();
const isTriggerMessageEventObject = new tg.IsInterface().withProperties({
type: tg.isSingletonString(triggerMessage),
data: isTriggerMessageEvent
}).get()
const isTriggerMessageRemoveEventObject = new tg.IsInterface().withProperties({
type: tg.isSingletonString(removeTriggerMessage),
data: isMessageReferenceEvent
}).get()
export const isTriggerMessageHandlerEvent = tg.isUnion(isTriggerMessageEventObject, isTriggerMessageRemoveEventObject)
export function triggerMessageEventHandler(event: tg.GuardedType<typeof isTriggerMessageHandlerEvent>) {
if (isTriggerMessageEventObject(event)) {
_triggerMessageEvent.next(event.data)
} else if (isTriggerMessageRemoveEventObject(event)) {
_removeTriggerMessageEvent.next(event.data)
}
}
export const isTriggerMessageHandlerEvent = tg.isUnion(isTriggerMessageEventObject, isTriggerMessageRemoveEventObject);

View file

@ -1,4 +1,5 @@
import { Subject } from "rxjs";
import type * as tg from "generic-type-guard";
import { ChatEvent, isChatEvent } from "./Events/ChatEvent";
import { HtmlUtils } from "../WebRtc/HtmlUtils";
import type { EnterLeaveEvent } from "./Events/EnterLeaveEvent";
@ -10,34 +11,40 @@ import { scriptUtils } from "./ScriptUtils";
import { GoToPageEvent, isGoToPageEvent } from "./Events/GoToPageEvent";
import { isOpenCoWebsite, OpenCoWebSiteEvent } from "./Events/OpenCoWebSiteEvent";
import {
IframeErrorAnswerEvent,
IframeEvent,
IframeEventMap,
IframeQuery,
IframeQueryMap,
IframeResponseEvent,
IframeResponseEventMap,
isIframeEventWrapper,
TypedMessageEvent
isIframeQueryWrapper,
TypedMessageEvent,
} from "./Events/IframeEvent";
import type { UserInputChatEvent } from "./Events/UserInputChatEvent";
//import { isLoadPageEvent } from './Events/LoadPageEvent';
import { isPlaySoundEvent, PlaySoundEvent } from "./Events/PlaySoundEvent";
import { isStopSoundEvent, StopSoundEvent } from "./Events/StopSoundEvent";
import { isLoadSoundEvent, LoadSoundEvent } from "./Events/LoadSoundEvent";
import { isSetPropertyEvent, SetPropertyEvent } from "./Events/setPropertyEvent";
import { isLayerEvent, LayerEvent } from "./Events/LayerEvent";
import { isMenuItemRegisterEvent, } from "./Events/ui/MenuItemRegisterEvent";
import { isMenuItemRegisterEvent } from "./Events/ui/MenuItemRegisterEvent";
import type { DataLayerEvent } from "./Events/DataLayerEvent";
import type { GameStateEvent } from "./Events/GameStateEvent";
import type { HasPlayerMovedEvent } from "./Events/HasPlayerMovedEvent";
import { isLoadPageEvent } from "./Events/LoadPageEvent";
import { handleMenuItemRegistrationEvent, isMenuItemRegisterIframeEvent } from "./Events/ui/MenuItemRegisterEvent";
import { isTriggerMessageHandlerEvent, triggerMessageEventHandler } from './Events/ui/TriggerMessageEventHandler';
import { SetTilesEvent, isSetTilesEvent } from "./Events/SetTilesEvent";
type AnswererCallback<T extends keyof IframeQueryMap> = (
query: IframeQueryMap[T]["query"]
) => IframeQueryMap[T]["answer"] | Promise<IframeQueryMap[T]["answer"]>;
/**
* Listens to messages from iframes and turn those messages into easy to use observables.
* Also allows to send messages to those iframes.
*/
class IframeListener {
private readonly _chatStream: Subject<ChatEvent> = new Subject();
public readonly chatStream = this._chatStream.asObservable();
@ -83,9 +90,6 @@ class IframeListener {
private readonly _setPropertyStream: Subject<SetPropertyEvent> = new Subject();
public readonly setPropertyStream = this._setPropertyStream.asObservable();
private readonly _gameStateStream: Subject<void> = new Subject();
public readonly gameStateStream = this._gameStateStream.asObservable();
private readonly _dataLayerChangeStream: Subject<void> = new Subject();
public readonly dataLayerChangeStream = this._dataLayerChangeStream.asObservable();
@ -104,113 +108,170 @@ class IframeListener {
private readonly _loadSoundStream: Subject<LoadSoundEvent> = new Subject();
public readonly loadSoundStream = this._loadSoundStream.asObservable();
private readonly _setTilesStream: Subject<SetTilesEvent> = new Subject();
public readonly setTilesStream = this._setTilesStream.asObservable();
private readonly iframes = new Set<HTMLIFrameElement>();
private readonly iframeCloseCallbacks = new Map<HTMLIFrameElement, (() => void)[]>();
private readonly scripts = new Map<string, HTMLIFrameElement>();
private sendPlayerMove: boolean = false;
private answerers: {
[key in keyof IframeQueryMap]?: AnswererCallback<key>;
} = {};
init() {
window.addEventListener("message", (message: TypedMessageEvent<IframeEvent<keyof IframeEventMap>>) => {
// Do we trust the sender of this message?
// Let's only accept messages from the iframe that are allowed.
// Note: maybe we could restrict on the domain too for additional security (in case the iframe goes to another domain).
let foundSrc: string | undefined;
window.addEventListener(
"message",
<T extends keyof IframeEventMap, U extends keyof IframeQueryMap>(
message: TypedMessageEvent<IframeEvent<T | U>>
) => {
// Do we trust the sender of this message?
// Let's only accept messages from the iframe that are allowed.
// Note: maybe we could restrict on the domain too for additional security (in case the iframe goes to another domain).
let foundSrc: string | undefined;
let iframe: HTMLIFrameElement;
for (iframe of this.iframes) {
if (iframe.contentWindow === message.source) {
foundSrc = iframe.src;
break;
}
}
if (foundSrc === undefined) {
return;
}
const payload = message.data;
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);
let iframe: HTMLIFrameElement | undefined;
for (iframe of this.iframes) {
if (iframe.contentWindow === message.source) {
foundSrc = iframe.src;
break;
}
}
else if (payload.type === 'closeCoWebSite') {
scriptUtils.closeCoWebSite();
const payload = message.data;
if (foundSrc === undefined || iframe === undefined) {
if (isIframeEventWrapper(payload)) {
console.warn(
"It seems an iFrame is trying to communicate with WorkAdventure but was not explicitly granted the permission to do so. " +
"If you are looking to use the WorkAdventure Scripting API inside an iFrame, you should allow the " +
'iFrame to communicate with WorkAdventure by using the "openWebsiteAllowApi" property in your map (or passing "true" as a second' +
"parameter to WA.nav.openCoWebSite())"
);
}
return;
}
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 == "getState") {
this._gameStateStream.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 (isTriggerMessageHandlerEvent(payload)) {
triggerMessageEventHandler(payload)
}
}
}, false);
foundSrc = this.getBaseUrl(foundSrc, message.source);
if (isIframeQueryWrapper(payload)) {
const queryId = payload.id;
const query = payload.query as IframeQuery<U>;
const answerer = this.answerers[query.type] as AnswererCallback<U> | undefined;
if (answerer === undefined) {
const errorMsg =
'The iFrame sent a message of type "' +
query.type +
'" but there is no service configured to answer these messages.';
console.error(errorMsg);
iframe.contentWindow?.postMessage(
{
id: queryId,
type: query.type,
error: errorMsg,
} as IframeErrorAnswerEvent,
"*"
);
return;
}
Promise.resolve(answerer(query.data))
.then((value) => {
iframe?.contentWindow?.postMessage(
{
id: queryId,
type: query.type,
data: value,
},
"*"
);
})
.catch((reason) => {
console.error("An error occurred while responding to an iFrame query.", reason);
let reasonMsg: string;
if (reason instanceof Error) {
reasonMsg = reason.message;
} else {
reasonMsg = reason.toString();
}
iframe?.contentWindow?.postMessage(
{
id: queryId,
type: query.type,
error: reasonMsg,
} as IframeErrorAnswerEvent,
"*"
);
});
} 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);
}
}
},
false
);
}
sendDataLayerEvent(dataLayerEvent: DataLayerEvent) {
this.postMessage({
'type': 'dataLayer',
'data': dataLayerEvent
})
}
sendGameStateEvent(gameStateEvent: GameStateEvent) {
this.postMessage({
'type': 'gameState',
'data': gameStateEvent
type: "dataLayer",
data: dataLayerEvent,
});
}
@ -223,25 +284,25 @@ class IframeListener {
}
unregisterIframe(iframe: HTMLIFrameElement): void {
this.iframeCloseCallbacks.get(iframe)?.forEach(callback => {
this.iframeCloseCallbacks.get(iframe)?.forEach((callback) => {
callback();
});
this.iframes.delete(iframe);
}
registerScript(scriptUrl: string): void {
console.log('Loading map related script at ', scriptUrl)
console.log("Loading map related script at ", scriptUrl);
if (!process.env.NODE_ENV || process.env.NODE_ENV === 'development') {
if (!process.env.NODE_ENV || process.env.NODE_ENV === "development") {
// Using external iframe mode (
const iframe = document.createElement('iframe');
const iframe = document.createElement("iframe");
iframe.id = IframeListener.getIFrameId(scriptUrl);
iframe.style.display = 'none';
iframe.src = '/iframe.html?script=' + encodeURIComponent(scriptUrl);
iframe.style.display = "none";
iframe.src = "/iframe.html?script=" + encodeURIComponent(scriptUrl);
// We are putting a sandbox on this script because it will run in the same domain as the main website.
iframe.sandbox.add('allow-scripts');
iframe.sandbox.add('allow-top-navigation-by-user-activation');
iframe.sandbox.add("allow-scripts");
iframe.sandbox.add("allow-top-navigation-by-user-activation");
document.body.prepend(iframe);
@ -249,36 +310,50 @@ class IframeListener {
this.registerIframe(iframe);
} else {
// production code
const iframe = document.createElement('iframe');
const iframe = document.createElement("iframe");
iframe.id = IframeListener.getIFrameId(scriptUrl);
iframe.style.display = 'none';
iframe.style.display = "none";
// We are putting a sandbox on this script because it will run in the same domain as the main website.
iframe.sandbox.add('allow-scripts');
iframe.sandbox.add('allow-top-navigation-by-user-activation');
iframe.sandbox.add("allow-scripts");
iframe.sandbox.add("allow-top-navigation-by-user-activation");
//iframe.src = "data:text/html;charset=utf-8," + escape(html);
iframe.srcdoc = '<!doctype html>\n' +
'\n' +
iframe.srcdoc =
"<!doctype html>\n" +
"\n" +
'<html lang="en">\n' +
'<head>\n' +
'<script src="' + window.location.protocol + '//' + window.location.host + '/iframe_api.js" ></script>\n' +
'<script src="' + scriptUrl + '" ></script>\n' +
'<title></title>\n' +
'</head>\n' +
'</html>\n';
"<head>\n" +
'<script src="' +
window.location.protocol +
"//" +
window.location.host +
'/iframe_api.js" ></script>\n' +
'<script src="' +
scriptUrl +
'" ></script>\n' +
"<title></title>\n" +
"</head>\n" +
"</html>\n";
document.body.prepend(iframe);
this.scripts.set(scriptUrl, iframe);
this.registerIframe(iframe);
}
}
private getBaseUrl(src: string, source: MessageEventSource | null): string {
for (const script of this.scripts) {
if (script[1].contentWindow === source) {
return script[0];
}
}
return src;
}
private static getIFrameId(scriptUrl: string): string {
return 'script' + btoa(scriptUrl);
return "script" + btoa(scriptUrl);
}
unregisterScript(scriptUrl: string): void {
@ -295,47 +370,47 @@ class IframeListener {
sendUserInputChat(message: string) {
this.postMessage({
'type': 'userInputChat',
'data': {
'message': message,
} as UserInputChatEvent
type: "userInputChat",
data: {
message: message,
} as UserInputChatEvent,
});
}
sendEnterEvent(name: string) {
this.postMessage({
'type': 'enterEvent',
'data': {
"name": name
} as EnterLeaveEvent
type: "enterEvent",
data: {
name: name,
} as EnterLeaveEvent,
});
}
sendLeaveEvent(name: string) {
this.postMessage({
'type': 'leaveEvent',
'data': {
"name": name
} as EnterLeaveEvent
type: "leaveEvent",
data: {
name: name,
} as EnterLeaveEvent,
});
}
hasPlayerMoved(event: HasPlayerMovedEvent) {
if (this.sendPlayerMove) {
this.postMessage({
'type': 'hasPlayerMoved',
'data': event
type: "hasPlayerMoved",
data: event,
});
}
}
sendButtonClickedEvent(popupId: number, buttonId: number): void {
this.postMessage({
'type': 'buttonClickedEvent',
'data': {
type: "buttonClickedEvent",
data: {
popupId,
buttonId
} as ButtonClickedEvent
buttonId,
} as ButtonClickedEvent,
});
}
@ -344,10 +419,30 @@ class IframeListener {
*/
public postMessage(message: IframeResponseEvent<keyof IframeResponseEventMap>) {
for (const iframe of this.iframes) {
iframe.contentWindow?.postMessage(message, '*');
iframe.contentWindow?.postMessage(message, "*");
}
}
/**
* Registers a callback that can be used to respond to some query (as defined in the IframeQueryMap type).
*
* Important! There can be only one "answerer" so registering a new one will unregister the old one.
*
* @param key The "type" of the query we are answering
* @param callback
*/
public registerAnswerer<T extends keyof IframeQueryMap, Guard extends tg.TypeGuard<IframeQueryMap[T]["query"]>>(
key: T,
callback: AnswererCallback<T>,
typeChecker?: Guard
): void {
//@ts-ignore
this.answerers[key] = callback;
}
public unregisterAnswerer(key: keyof IframeQueryMap): void {
delete this.answerers[key];
}
}
export const iframeListener = new IframeListener();

View file

@ -1,21 +1,19 @@
import {coWebsiteManager} from "../WebRtc/CoWebsiteManager";
import { coWebsiteManager } from "../WebRtc/CoWebsiteManager";
class ScriptUtils {
public openTab(url : string){
public openTab(url: string) {
window.open(url);
}
public goToPage(url : string){
window.location.href = url;
public goToPage(url: string) {
window.location.href = url;
}
public openCoWebsite(url: string, base: string) {
coWebsiteManager.loadCoWebsite(url, base);
public openCoWebsite(url: string, base: string, api: boolean, policy: string) {
coWebsiteManager.loadCoWebsite(url, base, api, policy);
}
public closeCoWebSite(){
public closeCoWebSite() {
coWebsiteManager.closeCoWebsite();
}
}

View file

@ -1,20 +1,66 @@
import type * as tg from "generic-type-guard";
import type { IframeEvent, IframeEventMap, IframeResponseEventMap } from '../Events/IframeEvent';
import type {
IframeEvent,
IframeEventMap,
IframeQuery,
IframeQueryMap,
IframeResponseEventMap,
} from "../Events/IframeEvent";
import type { IframeQueryWrapper } from "../Events/IframeEvent";
export function sendToWorkadventure(content: IframeEvent<keyof IframeEventMap>) {
window.parent.postMessage(content, "*")
window.parent.postMessage(content, "*");
}
type GuardedType<Guard extends tg.TypeGuard<unknown>> = Guard extends tg.TypeGuard<infer T> ? T : never
export interface IframeCallback<Key extends keyof IframeResponseEventMap, T = IframeResponseEventMap[Key], Guard = tg.TypeGuard<T>> {
let queryNumber = 0;
typeChecker: Guard,
callback: (payloadData: T) => void
export const answerPromises = new Map<
number,
{
resolve: (
value:
| IframeQueryMap[keyof IframeQueryMap]["answer"]
| PromiseLike<IframeQueryMap[keyof IframeQueryMap]["answer"]>
) => void;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
reject: (reason?: any) => void;
}
>();
export function queryWorkadventure<T extends keyof IframeQueryMap>(
content: IframeQuery<T>
): Promise<IframeQueryMap[T]["answer"]> {
return new Promise<IframeQueryMap[T]["answer"]>((resolve, reject) => {
window.parent.postMessage(
{
id: queryNumber,
query: content,
} as IframeQueryWrapper<T>,
"*"
);
answerPromises.set(queryNumber, {
resolve,
reject,
});
queryNumber++;
});
}
type GuardedType<Guard extends tg.TypeGuard<unknown>> = Guard extends tg.TypeGuard<infer T> ? T : never;
export interface IframeCallback<
Key extends keyof IframeResponseEventMap,
T = IframeResponseEventMap[Key],
Guard = tg.TypeGuard<T>
> {
typeChecker: Guard;
callback: (payloadData: T) => void;
}
export interface IframeCallbackContribution<Key extends keyof IframeResponseEventMap> extends IframeCallback<Key> {
type: Key
type: Key;
}
/**
@ -23,9 +69,10 @@ export interface IframeCallbackContribution<Key extends keyof IframeResponseEven
*
*/
export abstract class IframeApiContribution<T extends {
callbacks: Array<IframeCallbackContribution<keyof IframeResponseEventMap>>,
}> {
abstract callbacks: T["callbacks"]
export abstract class IframeApiContribution<
T extends {
callbacks: Array<IframeCallbackContribution<keyof IframeResponseEventMap>>;
}
> {
abstract callbacks: T["callbacks"];
}

View file

@ -1,11 +1,11 @@
import type { MenuItemClickedEvent } from '../../Events/ui/MenuItemClickedEvent';
import { iframeListener } from '../../IframeListener';
import type { MenuItemClickedEvent } from "../../Events/ui/MenuItemClickedEvent";
import { iframeListener } from "../../IframeListener";
export function sendMenuClickedEvent(menuItem: string) {
iframeListener.postMessage({
'type': 'menuItemClicked',
'data': {
type: "menuItemClicked",
data: {
menuItem: menuItem,
} as MenuItemClickedEvent
} as MenuItemClickedEvent,
});
}
}

View file

@ -1,22 +1,25 @@
import { removeTriggerMessage, triggerMessage, TriggerMessageEvent } from '../../Events/ui/TriggerMessageEvent';
import { sendToWorkadventure } from '../IframeApiContribution';
import {
MessageReferenceEvent,
removeTriggerMessage,
triggerMessage,
TriggerMessageEvent,
} from "../../Events/ui/TriggerMessageEvent";
import { queryWorkadventure } from "../IframeApiContribution";
function uuidv4() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
const r = Math.random() * 16 | 0, v = c === 'x' ? r : (r & 0x3 | 0x8);
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
const r = (Math.random() * 16) | 0,
v = c === "x" ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
}
export let triggerMessageInstance: TriggerMessage | undefined = undefined
export let triggerMessageInstance: TriggerMessage | undefined = undefined;
export class TriggerMessage {
uuid: string
uuid: string;
constructor(private message: string, private callback: () => void) {
this.uuid = uuidv4()
this.uuid = uuidv4();
if (triggerMessageInstance) {
triggerMessageInstance.remove();
}
@ -24,28 +27,24 @@ export class TriggerMessage {
this.create();
}
create(): this {
sendToWorkadventure({
async create() {
await queryWorkadventure({
type: triggerMessage,
data: {
message: this.message,
uuid: this.uuid
} as TriggerMessageEvent
})
return this
}
remove() {
sendToWorkadventure({
type: removeTriggerMessage,
data: {
uuid: this.uuid
} as TriggerMessageEvent
})
triggerMessageInstance = undefined
}
trigger() {
uuid: this.uuid,
} as TriggerMessageEvent,
});
this.callback();
}
}
async remove() {
await queryWorkadventure({
type: removeTriggerMessage,
data: {
uuid: this.uuid,
} as MessageReferenceEvent,
});
triggerMessageInstance = undefined;
}
}

View file

@ -1,30 +1,30 @@
import type { ChatEvent } from '../Events/ChatEvent'
import { isUserInputChatEvent, UserInputChatEvent } from '../Events/UserInputChatEvent'
import { IframeApiContribution, sendToWorkadventure } from './IframeApiContribution'
import type { ChatEvent } from "../Events/ChatEvent";
import { isUserInputChatEvent, UserInputChatEvent } from "../Events/UserInputChatEvent";
import { IframeApiContribution, sendToWorkadventure } from "./IframeApiContribution";
import { apiCallback } from "./registeredCallbacks";
import {Subject} from "rxjs";
import { Subject } from "rxjs";
const chatStream = new Subject<string>();
class WorkadventureChatCommands extends IframeApiContribution<WorkadventureChatCommands> {
callbacks = [apiCallback({
callback: (event: UserInputChatEvent) => {
chatStream.next(event.message);
},
type: "userInputChat",
typeChecker: isUserInputChatEvent
})]
export class WorkadventureChatCommands extends IframeApiContribution<WorkadventureChatCommands> {
callbacks = [
apiCallback({
callback: (event: UserInputChatEvent) => {
chatStream.next(event.message);
},
type: "userInputChat",
typeChecker: isUserInputChatEvent,
}),
];
sendChatMessage(message: string, author: string) {
sendToWorkadventure({
type: 'chat',
type: "chat",
data: {
'message': message,
'author': author
}
})
message: message,
author: author,
},
});
}
/**
@ -35,4 +35,4 @@ class WorkadventureChatCommands extends IframeApiContribution<WorkadventureChatC
}
}
export default new WorkadventureChatCommands()
export default new WorkadventureChatCommands();

View file

@ -1,16 +1,15 @@
import { IframeApiContribution, sendToWorkadventure } from './IframeApiContribution';
import { IframeApiContribution, sendToWorkadventure } from "./IframeApiContribution";
class WorkadventureControlsCommands extends IframeApiContribution<WorkadventureControlsCommands> {
callbacks = []
export class WorkadventureControlsCommands extends IframeApiContribution<WorkadventureControlsCommands> {
callbacks = [];
disablePlayerControls(): void {
sendToWorkadventure({ 'type': 'disablePlayerControls', data: null });
sendToWorkadventure({ type: "disablePlayerControls", data: null });
}
restorePlayerControls(): void {
sendToWorkadventure({ 'type': 'restorePlayerControls', data: null });
sendToWorkadventure({ type: "restorePlayerControls", data: null });
}
}
export default new WorkadventureControlsCommands();

View file

@ -1,57 +1,56 @@
import type { GoToPageEvent } from '../Events/GoToPageEvent';
import type { OpenTabEvent } from '../Events/OpenTabEvent';
import { IframeApiContribution, sendToWorkadventure } from './IframeApiContribution';
import type {OpenCoWebSiteEvent} from "../Events/OpenCoWebSiteEvent";
import type {LoadPageEvent} from "../Events/LoadPageEvent";
class WorkadventureNavigationCommands extends IframeApiContribution<WorkadventureNavigationCommands> {
callbacks = []
import type { GoToPageEvent } from "../Events/GoToPageEvent";
import type { OpenTabEvent } from "../Events/OpenTabEvent";
import { IframeApiContribution, sendToWorkadventure } from "./IframeApiContribution";
import type { OpenCoWebSiteEvent } from "../Events/OpenCoWebSiteEvent";
import type { LoadPageEvent } from "../Events/LoadPageEvent";
export class WorkadventureNavigationCommands extends IframeApiContribution<WorkadventureNavigationCommands> {
callbacks = [];
openTab(url: string): void {
sendToWorkadventure({
"type": 'openTab',
"data": {
url
}
type: "openTab",
data: {
url,
},
});
}
goToPage(url: string): void {
sendToWorkadventure({
"type": 'goToPage',
"data": {
url
}
type: "goToPage",
data: {
url,
},
});
}
goToRoom(url: string): void {
sendToWorkadventure({
"type": 'loadPage',
"data": {
url
}
type: "loadPage",
data: {
url,
},
});
}
openCoWebSite(url: string): void {
openCoWebSite(url: string, allowApi: boolean = false, allowPolicy: string = ""): void {
sendToWorkadventure({
"type": 'openCoWebSite',
"data": {
url
}
type: "openCoWebSite",
data: {
url,
allowApi,
allowPolicy,
},
});
}
closeCoWebSite(): void {
sendToWorkadventure({
"type": 'closeCoWebSite',
data: null
type: "closeCoWebSite",
data: null,
});
}
}
export default new WorkadventureNavigationCommands();

View file

@ -1,29 +1,29 @@
import {IframeApiContribution, sendToWorkadventure} from "./IframeApiContribution";
import type {HasPlayerMovedEvent, HasPlayerMovedEventCallback} from "../Events/HasPlayerMovedEvent";
import {Subject} from "rxjs";
import {apiCallback} from "./registeredCallbacks";
import {isHasPlayerMovedEvent} from "../Events/HasPlayerMovedEvent";
import { IframeApiContribution, sendToWorkadventure } from "./IframeApiContribution";
import type { HasPlayerMovedEvent, HasPlayerMovedEventCallback } from "../Events/HasPlayerMovedEvent";
import { Subject } from "rxjs";
import { apiCallback } from "./registeredCallbacks";
import { isHasPlayerMovedEvent } from "../Events/HasPlayerMovedEvent";
const moveStream = new Subject<HasPlayerMovedEvent>();
class WorkadventurePlayerCommands extends IframeApiContribution<WorkadventurePlayerCommands> {
export class WorkadventurePlayerCommands extends IframeApiContribution<WorkadventurePlayerCommands> {
callbacks = [
apiCallback({
type: 'hasPlayerMoved',
type: "hasPlayerMoved",
typeChecker: isHasPlayerMovedEvent,
callback: (payloadData) => {
moveStream.next(payloadData);
}
},
}),
]
];
onPlayerMove(callback: HasPlayerMovedEventCallback): void {
moveStream.subscribe(callback);
sendToWorkadventure({
type: 'onPlayerMove',
data: null
})
type: "onPlayerMove",
data: null,
});
}
}
export default new WorkadventurePlayerCommands();
export default new WorkadventurePlayerCommands();

View file

@ -1,87 +1,81 @@
import { Subject } from "rxjs";
import { EnterLeaveEvent, isEnterLeaveEvent } from '../Events/EnterLeaveEvent';
import {IframeApiContribution, sendToWorkadventure} from './IframeApiContribution';
import { isDataLayerEvent } from "../Events/DataLayerEvent";
import { EnterLeaveEvent, isEnterLeaveEvent } from "../Events/EnterLeaveEvent";
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 type {GameStateEvent} from "../Events/GameStateEvent";
import type {ITiledMap} from "../../Phaser/Map/ITiledMap";
import type {DataLayerEvent} from "../Events/DataLayerEvent";
import {isGameStateEvent} from "../Events/GameStateEvent";
import {isDataLayerEvent} from "../Events/DataLayerEvent";
import type { ITiledMap } from "../../Phaser/Map/ITiledMap";
import type { DataLayerEvent } from "../Events/DataLayerEvent";
import type { GameStateEvent } from "../Events/GameStateEvent";
const enterStreams: 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 stateResolvers = new Subject<GameStateEvent>();
let immutableData: GameStateEvent;
let immutableDataPromise: Promise<GameStateEvent> | undefined = undefined;
interface Room {
id: string,
mapUrl: string,
map: ITiledMap,
startLayer: string | null
id: string;
mapUrl: string;
map: ITiledMap;
startLayer: string | null;
}
interface User {
id: string | undefined,
nickName: string | null,
tags: string[]
id: string | undefined;
nickName: string | null;
tags: string[];
}
interface TileDescriptor {
x: number;
y: number;
tile: number | string;
layer: string;
}
function getGameState(): Promise<GameStateEvent> {
if (immutableData) {
return Promise.resolve(immutableData);
}
else {
return new Promise<GameStateEvent>((resolver, thrower) => {
stateResolvers.subscribe(resolver);
sendToWorkadventure({type: "getState", data: null});
})
if (immutableDataPromise === undefined) {
immutableDataPromise = queryWorkadventure({ type: "getState", data: undefined });
}
return immutableDataPromise;
}
function getDataLayer(): Promise<DataLayerEvent> {
return new Promise<DataLayerEvent>((resolver, thrower) => {
dataLayerResolver.subscribe(resolver);
sendToWorkadventure({type: "getDataLayer", data: null})
})
sendToWorkadventure({ type: "getDataLayer", data: null });
});
}
class WorkadventureRoomCommands extends IframeApiContribution<WorkadventureRoomCommands> {
export class WorkadventureRoomCommands extends IframeApiContribution<WorkadventureRoomCommands> {
callbacks = [
apiCallback({
callback: (payloadData: EnterLeaveEvent) => {
enterStreams.get(payloadData.name)?.next();
},
type: "enterEvent",
typeChecker: isEnterLeaveEvent
typeChecker: isEnterLeaveEvent,
}),
apiCallback({
type: "leaveEvent",
typeChecker: isEnterLeaveEvent,
callback: (payloadData) => {
leaveStreams.get(payloadData.name)?.next();
}
}),
apiCallback({
type: "gameState",
typeChecker: isGameStateEvent,
callback: (payloadData) => {
stateResolvers.next(payloadData);
}
},
}),
apiCallback({
type: "dataLayer",
typeChecker: isDataLayerEvent,
callback: (payloadData) => {
dataLayerResolver.next(payloadData);
}
},
}),
]
];
onEnterZone(name: string, callback: () => void): void {
let subject = enterStreams.get(name);
@ -90,7 +84,6 @@ class WorkadventureRoomCommands extends IframeApiContribution<WorkadventureRoomC
enterStreams.set(name, subject);
}
subject.subscribe(callback);
}
onLeaveZone(name: string, callback: () => void): void {
let subject = leaveStreams.get(name);
@ -101,35 +94,44 @@ class WorkadventureRoomCommands extends IframeApiContribution<WorkadventureRoomC
subject.subscribe(callback);
}
showLayer(layerName: string): void {
sendToWorkadventure({type: 'showLayer', data: {'name': layerName}});
sendToWorkadventure({ type: "showLayer", data: { name: layerName } });
}
hideLayer(layerName: string): void {
sendToWorkadventure({type: 'hideLayer', data: {'name': layerName}});
sendToWorkadventure({ type: "hideLayer", data: { name: layerName } });
}
setProperty(layerName: string, propertyName: string, propertyValue: string | number | boolean | undefined): void {
sendToWorkadventure({
type: 'setProperty',
type: "setProperty",
data: {
'layerName': layerName,
'propertyName': propertyName,
'propertyValue': propertyValue,
}
})
layerName: layerName,
propertyName: propertyName,
propertyValue: propertyValue,
},
});
}
getCurrentRoom(): Promise<Room> {
return getGameState().then((gameState) => {
return getDataLayer().then((mapJson) => {
return {id: gameState.roomId, map: mapJson.data as ITiledMap, mapUrl: gameState.mapUrl, startLayer: gameState.startLayerName};
})
})
return getDataLayer().then((mapJson) => {
return {
id: gameState.roomId,
map: mapJson.data as ITiledMap,
mapUrl: gameState.mapUrl,
startLayer: gameState.startLayerName,
};
});
});
}
getCurrentUser(): Promise<User> {
return getGameState().then((gameState) => {
return {id: gameState.uuid, nickName: gameState.nickname, tags: gameState.tags};
})
return { id: gameState.uuid, nickName: gameState.nickname, tags: gameState.tags };
});
}
setTiles(tiles: TileDescriptor[]) {
sendToWorkadventure({
type: "setTiles",
data: tiles,
});
}
}
export default new WorkadventureRoomCommands();

View file

@ -1,17 +1,15 @@
import type { LoadSoundEvent } from '../Events/LoadSoundEvent';
import type { PlaySoundEvent } from '../Events/PlaySoundEvent';
import type { StopSoundEvent } from '../Events/StopSoundEvent';
import { IframeApiContribution, sendToWorkadventure } from './IframeApiContribution';
import {Sound} from "./Sound/Sound";
import type { LoadSoundEvent } from "../Events/LoadSoundEvent";
import type { PlaySoundEvent } from "../Events/PlaySoundEvent";
import type { StopSoundEvent } from "../Events/StopSoundEvent";
import { IframeApiContribution, sendToWorkadventure } from "./IframeApiContribution";
import { Sound } from "./Sound/Sound";
class WorkadventureSoundCommands extends IframeApiContribution<WorkadventureSoundCommands> {
callbacks = []
export class WorkadventureSoundCommands extends IframeApiContribution<WorkadventureSoundCommands> {
callbacks = [];
loadSound(url: string): Sound {
return new Sound(url);
}
}
export default new WorkadventureSoundCommands();

View file

@ -1,20 +1,29 @@
import { isButtonClickedEvent } from '../Events/ButtonClickedEvent';
import { isMenuItemClickedEvent } from '../Events/ui/MenuItemClickedEvent';
import { isMessageReferenceEvent } from '../Events/ui/TriggerMessageEvent';
import { IframeApiContribution, sendToWorkadventure } from './IframeApiContribution';
import { isButtonClickedEvent } from "../Events/ButtonClickedEvent";
import { isMenuItemClickedEvent } from "../Events/ui/MenuItemClickedEvent";
import { IframeApiContribution, sendToWorkadventure } from "./IframeApiContribution";
import { apiCallback } from "./registeredCallbacks";
import type { ButtonClickedCallback, ButtonDescriptor } from "./Ui/ButtonDescriptor";
import { Popup } from "./Ui/Popup";
import { TriggerMessage, triggerMessageInstance } from './Ui/TriggerMessage';
import { TriggerMessage } from "./Ui/TriggerMessage";
let popupId = 0;
const popups: Map<number, Popup> = new Map<number, Popup>();
const popupCallbacks: Map<number, Map<number, ButtonClickedCallback>> = new Map<number, Map<number, ButtonClickedCallback>>();
const popupCallbacks: Map<number, Map<number, ButtonClickedCallback>> = new Map<
number,
Map<number, ButtonClickedCallback>
>();
const menuCallbacks: Map<string, (command: string) => void> = new Map()
const menuCallbacks: Map<string, (command: string) => void> = new Map();
class WorkAdventureUiCommands extends IframeApiContribution<WorkAdventureUiCommands> {
interface ZonedPopupOptions {
zone: string;
objectLayerName?: string;
popupText: string;
delay?: number;
popupOptions: Array<ButtonDescriptor>;
}
export class WorkAdventureUiCommands extends IframeApiContribution<WorkAdventureUiCommands> {
callbacks = [
apiCallback({
type: "buttonClickedEvent",
@ -28,28 +37,20 @@ class WorkAdventureUiCommands extends IframeApiContribution<WorkAdventureUiComma
if (callback) {
callback(popup);
}
}
},
}),
apiCallback({
type: "menuItemClicked",
typeChecker: isMenuItemClickedEvent,
callback: event => {
callback: (event) => {
const callback = menuCallbacks.get(event.menuItem);
if (callback) {
callback(event.menuItem)
callback(event.menuItem);
}
}
},
}),
apiCallback({
type: "messageTriggered",
typeChecker: isMessageReferenceEvent,
callback: event => {
triggerMessageInstance?.trigger();
}
})
];
openPopup(targetObject: string, message: string, buttons: ButtonDescriptor[]): Popup {
popupId++;
const popup = new Popup(popupId);
@ -67,41 +68,42 @@ class WorkAdventureUiCommands extends IframeApiContribution<WorkAdventureUiComma
}
sendToWorkadventure({
'type': 'openPopup',
'data': {
type: "openPopup",
data: {
popupId,
targetObject,
message,
buttons: buttons.map((button) => {
return {
label: button.label,
className: button.className
className: button.className,
};
})
}
}),
},
});
popups.set(popupId, popup)
popups.set(popupId, popup);
return popup;
}
registerMenuCommand(commandDescriptor: string, callback: (commandDescriptor: string) => void) {
menuCallbacks.set(commandDescriptor, callback);
sendToWorkadventure({
'type': 'registerMenuCommand',
'data': {
menutItem: commandDescriptor
}
type: "registerMenuCommand",
data: {
menutItem: commandDescriptor,
},
});
}
displayBubble(): void {
sendToWorkadventure({ 'type': 'displayBubble', data: null });
sendToWorkadventure({ type: "displayBubble", data: null });
}
removeBubble(): void {
sendToWorkadventure({ 'type': 'removeBubble', data: null });
sendToWorkadventure({ type: "removeBubble", data: null });
}
triggerMessage(message: string, callback: () => void): TriggerMessage {
return new TriggerMessage(message, callback);
}