Merge branch 'develop' of github.com:thecodingmachine/workadventure into GlobalMessageToWorld
This commit is contained in:
commit
68ff1b9e17
81 changed files with 3001 additions and 1073 deletions
|
@ -4,10 +4,11 @@ export const isGameStateEvent = new tg.IsInterface()
|
|||
.withProperties({
|
||||
roomId: tg.isString,
|
||||
mapUrl: tg.isString,
|
||||
nickname: tg.isUnion(tg.isString, tg.isNull),
|
||||
nickname: tg.isString,
|
||||
uuid: tg.isUnion(tg.isString, tg.isUndefined),
|
||||
startLayerName: tg.isUnion(tg.isString, tg.isNull),
|
||||
tags: tg.isArray(tg.isString),
|
||||
variables: tg.isObject,
|
||||
})
|
||||
.get();
|
||||
/**
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import * as tg from "generic-type-guard";
|
||||
import type { GameStateEvent } from "./GameStateEvent";
|
||||
import type { ButtonClickedEvent } from "./ButtonClickedEvent";
|
||||
import type { ChatEvent } from "./ChatEvent";
|
||||
|
@ -9,7 +10,7 @@ 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 { MapDataEvent } from "./MapDataEvent";
|
||||
import type { LayerEvent } from "./LayerEvent";
|
||||
import type { SetPropertyEvent } from "./setPropertyEvent";
|
||||
import type { LoadSoundEvent } from "./LoadSoundEvent";
|
||||
|
@ -18,6 +19,10 @@ 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 { isGameStateEvent } from "./GameStateEvent";
|
||||
import { isMapDataEvent } from "./MapDataEvent";
|
||||
import { isSetVariableEvent } from "./SetVariableEvent";
|
||||
|
||||
export interface TypedMessageEvent<T> extends MessageEvent {
|
||||
data: T;
|
||||
|
@ -43,7 +48,6 @@ export type IframeEventMap = {
|
|||
showLayer: LayerEvent;
|
||||
hideLayer: LayerEvent;
|
||||
setProperty: SetPropertyEvent;
|
||||
getDataLayer: undefined;
|
||||
loadSound: LoadSoundEvent;
|
||||
playSound: PlaySoundEvent;
|
||||
stopSound: null;
|
||||
|
@ -66,8 +70,8 @@ export interface IframeResponseEventMap {
|
|||
leaveEvent: EnterLeaveEvent;
|
||||
buttonClickedEvent: ButtonClickedEvent;
|
||||
hasPlayerMoved: HasPlayerMovedEvent;
|
||||
dataLayer: DataLayerEvent;
|
||||
menuItemClicked: MenuItemClickedEvent;
|
||||
setVariable: SetVariableEvent;
|
||||
}
|
||||
export interface IframeResponseEvent<T extends keyof IframeResponseEventMap> {
|
||||
type: T;
|
||||
|
@ -79,20 +83,39 @@ 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
|
||||
* 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.
|
||||
* Types are defined using Type guards that will actually bused to enforce and check types.
|
||||
*/
|
||||
export type IframeQueryMap = {
|
||||
export const iframeQueryMapTypeGuards = {
|
||||
getState: {
|
||||
query: undefined,
|
||||
answer: GameStateEvent
|
||||
query: tg.isUndefined,
|
||||
answer: isGameStateEvent,
|
||||
},
|
||||
}
|
||||
getMapData: {
|
||||
query: tg.isUndefined,
|
||||
answer: isMapDataEvent,
|
||||
},
|
||||
setVariable: {
|
||||
query: isSetVariableEvent,
|
||||
answer: tg.isUndefined,
|
||||
},
|
||||
};
|
||||
|
||||
type GuardedType<T> = T extends (x: unknown) => x is infer T ? T : never;
|
||||
type IframeQueryMapTypeGuardsType = typeof iframeQueryMapTypeGuards;
|
||||
type UnknownToVoid<T> = undefined extends T ? void : T;
|
||||
|
||||
export type IframeQueryMap = {
|
||||
[key in keyof IframeQueryMapTypeGuardsType]: {
|
||||
query: GuardedType<IframeQueryMapTypeGuardsType[key]["query"]>;
|
||||
answer: UnknownToVoid<GuardedType<IframeQueryMapTypeGuardsType[key]["answer"]>>;
|
||||
};
|
||||
};
|
||||
|
||||
export interface IframeQuery<T extends keyof IframeQueryMap> {
|
||||
type: T;
|
||||
data: IframeQueryMap[T]['query'];
|
||||
data: IframeQueryMap[T]["query"];
|
||||
}
|
||||
|
||||
export interface IframeQueryWrapper<T extends keyof IframeQueryMap> {
|
||||
|
@ -100,19 +123,36 @@ export interface IframeQueryWrapper<T extends keyof IframeQueryMap> {
|
|||
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';
|
||||
export const isIframeQueryKey = (type: string): type is keyof IframeQueryMap => {
|
||||
return type in iframeQueryMapTypeGuards;
|
||||
};
|
||||
|
||||
// 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 const isIframeQuery = (event: any): event is IframeQuery<keyof IframeQueryMap> => {
|
||||
const type = event.type;
|
||||
if (typeof type !== "string") {
|
||||
return false;
|
||||
}
|
||||
if (!isIframeQueryKey(type)) {
|
||||
return false;
|
||||
}
|
||||
return iframeQueryMapTypeGuards[type].query(event.data);
|
||||
};
|
||||
|
||||
// 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'];
|
||||
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 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;
|
||||
|
@ -120,4 +160,9 @@ export interface IframeErrorAnswerEvent {
|
|||
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';
|
||||
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";
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import * as tg from "generic-type-guard";
|
||||
|
||||
export const isDataLayerEvent = new tg.IsInterface()
|
||||
export const isMapDataEvent = new tg.IsInterface()
|
||||
.withProperties({
|
||||
data: tg.isObject,
|
||||
})
|
||||
|
@ -9,4 +9,4 @@ export const isDataLayerEvent = new tg.IsInterface()
|
|||
/**
|
||||
* 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 MapDataEvent = tg.GuardedType<typeof isMapDataEvent>;
|
20
front/src/Api/Events/SetVariableEvent.ts
Normal file
20
front/src/Api/Events/SetVariableEvent.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
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();
|
|
@ -12,7 +12,8 @@ import { isOpenCoWebsite, OpenCoWebSiteEvent } from "./Events/OpenCoWebSiteEvent
|
|||
import {
|
||||
IframeErrorAnswerEvent,
|
||||
IframeEvent,
|
||||
IframeEventMap, IframeQueryMap,
|
||||
IframeEventMap,
|
||||
IframeQueryMap,
|
||||
IframeResponseEvent,
|
||||
IframeResponseEventMap,
|
||||
isIframeEventWrapper,
|
||||
|
@ -26,20 +27,26 @@ 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 type { DataLayerEvent } from "./Events/DataLayerEvent";
|
||||
import type { MapDataEvent } from "./Events/MapDataEvent";
|
||||
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 { 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"] | PromiseLike<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 _readyStream: Subject<HTMLIFrameElement> = new Subject();
|
||||
public readonly readyStream = this._readyStream.asObservable();
|
||||
|
||||
private readonly _chatStream: Subject<ChatEvent> = new Subject();
|
||||
public readonly chatStream = this._chatStream.asObservable();
|
||||
|
||||
|
@ -85,9 +92,6 @@ class IframeListener {
|
|||
private readonly _setPropertyStream: Subject<SetPropertyEvent> = new Subject();
|
||||
public readonly setPropertyStream = this._setPropertyStream.asObservable();
|
||||
|
||||
private readonly _dataLayerChangeStream: Subject<void> = new Subject();
|
||||
public readonly dataLayerChangeStream = this._dataLayerChangeStream.asObservable();
|
||||
|
||||
private readonly _registerMenuCommandStream: Subject<string> = new Subject();
|
||||
public readonly registerMenuCommandStream = this._registerMenuCommandStream.asObservable();
|
||||
|
||||
|
@ -111,8 +115,9 @@ class IframeListener {
|
|||
private readonly scripts = new Map<string, HTMLIFrameElement>();
|
||||
private sendPlayerMove: boolean = false;
|
||||
|
||||
// Note: we are forced to type this in unknown and later cast with "as" because of https://github.com/microsoft/TypeScript/issues/31904
|
||||
private answerers: {
|
||||
[key in keyof IframeQueryMap]?: AnswererCallback<key>
|
||||
[str in keyof IframeQueryMap]?: unknown;
|
||||
} = {};
|
||||
|
||||
init() {
|
||||
|
@ -152,109 +157,136 @@ class IframeListener {
|
|||
const queryId = payload.id;
|
||||
const query = payload.query;
|
||||
|
||||
const answerer = this.answerers[query.type];
|
||||
const answerer = this.answerers[query.type] as AnswererCallback<keyof IframeQueryMap> | 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.';
|
||||
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, '*');
|
||||
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;
|
||||
const errorHandler = (reason: unknown) => {
|
||||
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();
|
||||
} else if (typeof reason === "object") {
|
||||
reasonMsg = reason ? reason.toString() : "";
|
||||
} else if (typeof reason === "string") {
|
||||
reasonMsg = reason;
|
||||
}
|
||||
|
||||
iframe?.contentWindow?.postMessage({
|
||||
id: queryId,
|
||||
type: query.type,
|
||||
error: reasonMsg
|
||||
} as IframeErrorAnswerEvent, '*');
|
||||
});
|
||||
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);
|
||||
try {
|
||||
Promise.resolve(answerer(query.data))
|
||||
.then((value) => {
|
||||
iframe?.contentWindow?.postMessage(
|
||||
{
|
||||
id: queryId,
|
||||
type: query.type,
|
||||
data: value,
|
||||
},
|
||||
"*"
|
||||
);
|
||||
})
|
||||
.catch(errorHandler);
|
||||
} catch (reason) {
|
||||
errorHandler(reason);
|
||||
}
|
||||
|
||||
if (isSetVariableIframeEvent(payload.query)) {
|
||||
// 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.query.data,
|
||||
},
|
||||
"*"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} 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 (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,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows the passed iFrame to send/receive messages via the API.
|
||||
*/
|
||||
|
@ -394,6 +426,13 @@ class IframeListener {
|
|||
});
|
||||
}
|
||||
|
||||
setVariable(setVariableEvent: SetVariableEvent) {
|
||||
this.postMessage({
|
||||
type: "setVariable",
|
||||
data: setVariableEvent,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the message... to all allowed iframes.
|
||||
*/
|
||||
|
@ -411,7 +450,7 @@ class IframeListener {
|
|||
* @param key The "type" of the query we are answering
|
||||
* @param callback
|
||||
*/
|
||||
public registerAnswerer<T extends keyof IframeQueryMap>(key: T, callback: (query: IframeQueryMap[T]['query']) => IframeQueryMap[T]['answer']|Promise<IframeQueryMap[T]['answer']> ): void {
|
||||
public registerAnswerer<T extends keyof IframeQueryMap>(key: T, callback: AnswererCallback<T>): void {
|
||||
this.answerers[key] = callback;
|
||||
}
|
||||
|
||||
|
|
|
@ -2,17 +2,28 @@ import { IframeApiContribution, sendToWorkadventure } from "./IframeApiContribut
|
|||
import type { HasPlayerMovedEvent, HasPlayerMovedEventCallback } from "../Events/HasPlayerMovedEvent";
|
||||
import { Subject } from "rxjs";
|
||||
import { apiCallback } from "./registeredCallbacks";
|
||||
import { getGameState } from "./room";
|
||||
import { isHasPlayerMovedEvent } from "../Events/HasPlayerMovedEvent";
|
||||
|
||||
interface User {
|
||||
id: string | undefined;
|
||||
nickName: string | null;
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
const moveStream = new Subject<HasPlayerMovedEvent>();
|
||||
|
||||
let playerName: string | undefined;
|
||||
|
||||
export const setPlayerName = (name: string) => {
|
||||
playerName = name;
|
||||
};
|
||||
|
||||
let tags: string[] | undefined;
|
||||
|
||||
export const setTags = (_tags: string[]) => {
|
||||
tags = _tags;
|
||||
};
|
||||
|
||||
let uuid: string | undefined;
|
||||
|
||||
export const setUuid = (_uuid: string | undefined) => {
|
||||
uuid = _uuid;
|
||||
};
|
||||
|
||||
export class WorkadventurePlayerCommands extends IframeApiContribution<WorkadventurePlayerCommands> {
|
||||
callbacks = [
|
||||
apiCallback({
|
||||
|
@ -31,10 +42,30 @@ export class WorkadventurePlayerCommands extends IframeApiContribution<Workadven
|
|||
data: null,
|
||||
});
|
||||
}
|
||||
getCurrentUser(): Promise<User> {
|
||||
return getGameState().then((gameState) => {
|
||||
return { id: gameState.uuid, nickName: gameState.nickname, tags: gameState.tags };
|
||||
});
|
||||
|
||||
get name(): string {
|
||||
if (playerName === undefined) {
|
||||
throw new Error(
|
||||
"Player name not initialized yet. You should call WA.player.name within a WA.onInit callback."
|
||||
);
|
||||
}
|
||||
return playerName;
|
||||
}
|
||||
|
||||
get tags(): string[] {
|
||||
if (tags === undefined) {
|
||||
throw new Error("Tags not initialized yet. You should call WA.player.tags within a WA.onInit callback.");
|
||||
}
|
||||
return tags;
|
||||
}
|
||||
|
||||
get id(): string | undefined {
|
||||
// Note: this is not a type, we are checking if playerName is undefined because playerName cannot be undefined
|
||||
// while uuid could.
|
||||
if (playerName === undefined) {
|
||||
throw new Error("Player id not initialized yet. You should call WA.player.id within a WA.onInit callback.");
|
||||
}
|
||||
return uuid;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,28 +1,14 @@
|
|||
import { Subject } from "rxjs";
|
||||
import { Observable, Subject } from "rxjs";
|
||||
|
||||
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 { 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>();
|
||||
|
||||
let immutableDataPromise: Promise<GameStateEvent> | undefined = undefined;
|
||||
|
||||
interface Room {
|
||||
id: string;
|
||||
mapUrl: string;
|
||||
map: ITiledMap;
|
||||
startLayer: string | null;
|
||||
}
|
||||
|
||||
interface TileDescriptor {
|
||||
x: number;
|
||||
|
@ -31,19 +17,17 @@ interface TileDescriptor {
|
|||
layer: string;
|
||||
}
|
||||
|
||||
export function getGameState(): Promise<GameStateEvent> {
|
||||
if (immutableDataPromise === undefined) {
|
||||
immutableDataPromise = queryWorkadventure({ type: "getState", data: undefined });
|
||||
}
|
||||
return immutableDataPromise;
|
||||
}
|
||||
let roomId: string | undefined;
|
||||
|
||||
function getDataLayer(): Promise<DataLayerEvent> {
|
||||
return new Promise<DataLayerEvent>((resolver, thrower) => {
|
||||
dataLayerResolver.subscribe(resolver);
|
||||
sendToWorkadventure({ type: "getDataLayer", data: null });
|
||||
});
|
||||
}
|
||||
export const setRoomId = (id: string) => {
|
||||
roomId = id;
|
||||
};
|
||||
|
||||
let mapURL: string | undefined;
|
||||
|
||||
export const setMapURL = (url: string) => {
|
||||
mapURL = url;
|
||||
};
|
||||
|
||||
export class WorkadventureRoomCommands extends IframeApiContribution<WorkadventureRoomCommands> {
|
||||
callbacks = [
|
||||
|
@ -61,13 +45,6 @@ export class WorkadventureRoomCommands extends IframeApiContribution<Workadventu
|
|||
leaveStreams.get(payloadData.name)?.next();
|
||||
},
|
||||
}),
|
||||
apiCallback({
|
||||
type: "dataLayer",
|
||||
typeChecker: isDataLayerEvent,
|
||||
callback: (payloadData) => {
|
||||
dataLayerResolver.next(payloadData);
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
||||
onEnterZone(name: string, callback: () => void): void {
|
||||
|
@ -102,17 +79,9 @@ export class WorkadventureRoomCommands extends IframeApiContribution<Workadventu
|
|||
},
|
||||
});
|
||||
}
|
||||
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,
|
||||
};
|
||||
});
|
||||
});
|
||||
async getTiledMap(): Promise<ITiledMap> {
|
||||
const event = await queryWorkadventure({ type: "getMapData", data: undefined });
|
||||
return event.data as ITiledMap;
|
||||
}
|
||||
setTiles(tiles: TileDescriptor[]) {
|
||||
sendToWorkadventure({
|
||||
|
@ -120,6 +89,22 @@ export class WorkadventureRoomCommands extends IframeApiContribution<Workadventu
|
|||
data: tiles,
|
||||
});
|
||||
}
|
||||
|
||||
get id(): string {
|
||||
if (roomId === undefined) {
|
||||
throw new Error("Room id not initialized yet. You should call WA.room.id within a WA.onInit callback.");
|
||||
}
|
||||
return roomId;
|
||||
}
|
||||
|
||||
get mapURL(): string {
|
||||
if (mapURL === undefined) {
|
||||
throw new Error(
|
||||
"mapURL is not initialized yet. You should call WA.room.mapURL within a WA.onInit callback."
|
||||
);
|
||||
}
|
||||
return mapURL;
|
||||
}
|
||||
}
|
||||
|
||||
export default new WorkadventureRoomCommands();
|
||||
|
|
90
front/src/Api/iframe/state.ts
Normal file
90
front/src/Api/iframe/state.ts
Normal file
|
@ -0,0 +1,90 @@
|
|||
import { Observable, Subject } from "rxjs";
|
||||
|
||||
import { EnterLeaveEvent, isEnterLeaveEvent } from "../Events/EnterLeaveEvent";
|
||||
|
||||
import { IframeApiContribution, queryWorkadventure, sendToWorkadventure } from "./IframeApiContribution";
|
||||
import { apiCallback } from "./registeredCallbacks";
|
||||
import { isSetVariableEvent, SetVariableEvent } from "../Events/SetVariableEvent";
|
||||
|
||||
import type { ITiledMap } from "../../Phaser/Map/ITiledMap";
|
||||
|
||||
const setVariableResolvers = new Subject<SetVariableEvent>();
|
||||
const variables = new Map<string, unknown>();
|
||||
const variableSubscribers = new Map<string, Subject<unknown>>();
|
||||
|
||||
export const initVariables = (_variables: Map<string, unknown>): void => {
|
||||
for (const [name, value] of _variables.entries()) {
|
||||
// In case the user already decided to put values in the variables (before onInit), let's make sure onInit does not override this.
|
||||
if (!variables.has(name)) {
|
||||
variables.set(name, value);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
setVariableResolvers.subscribe((event) => {
|
||||
const oldValue = variables.get(event.key);
|
||||
|
||||
// If we are setting the same value, no need to do anything.
|
||||
if (oldValue === event.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
variables.set(event.key, event.value);
|
||||
const subject = variableSubscribers.get(event.key);
|
||||
if (subject !== undefined) {
|
||||
subject.next(event.value);
|
||||
}
|
||||
});
|
||||
|
||||
export class WorkadventureStateCommands extends IframeApiContribution<WorkadventureStateCommands> {
|
||||
callbacks = [
|
||||
apiCallback({
|
||||
type: "setVariable",
|
||||
typeChecker: isSetVariableEvent,
|
||||
callback: (payloadData) => {
|
||||
setVariableResolvers.next(payloadData);
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
||||
saveVariable(key: string, value: unknown): Promise<void> {
|
||||
variables.set(key, value);
|
||||
return queryWorkadventure({
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
const proxyCommand = new Proxy(new WorkadventureStateCommands(), {
|
||||
get(target: WorkadventureStateCommands, p: PropertyKey, receiver: unknown): unknown {
|
||||
if (p in target) {
|
||||
return Reflect.get(target, p, receiver);
|
||||
}
|
||||
return target.loadVariable(p.toString());
|
||||
},
|
||||
set(target: WorkadventureStateCommands, p: PropertyKey, value: unknown, receiver: unknown): boolean {
|
||||
// Note: when using "set", there is no way to wait, so we ignore the return of the promise.
|
||||
// User must use WA.state.saveVariable to have error message.
|
||||
target.saveVariable(p.toString(), value);
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
export default proxyCommand;
|
Loading…
Add table
Add a link
Reference in a new issue