diff --git a/docs/maps/api-player.md b/docs/maps/api-player.md index f483731e..ed73c32d 100644 --- a/docs/maps/api-player.md +++ b/docs/maps/api-player.md @@ -1,6 +1,63 @@ {.section-title.accent.text-primary} # API Player functions Reference +### Get the player name + +``` +WA.player.name: string; +``` + +The player name is available from the `WA.player.name` property. + +{.alert.alert-info} +You need to wait for the end of the initialization before accessing `WA.player.name` + +```typescript +WA.onInit().then(() => { + console.log('Player name: ', WA.player.name); +}) +``` + +### Get the player ID + +``` +WA.player.id: string|undefined; +``` + +The player ID is available from the `WA.player.id` property. +This is a unique identifier for a given player. Anonymous player might not have an id. + +{.alert.alert-info} +You need to wait for the end of the initialization before accessing `WA.player.id` + +```typescript +WA.onInit().then(() => { + console.log('Player ID: ', WA.player.id); +}) +``` + +### Get the tags of the player + +``` +WA.player.tags: string[]; +``` + +The player tags are available from the `WA.player.tags` property. +They represent a set of rights the player acquires after login in. + +{.alert.alert-warn} +Tags attributed to a user depend on the authentication system you are using. For the hosted version +of WorkAdventure, you can define tags related to the user in the [administration panel](https://workadventu.re/admin-guide/manage-members). + +{.alert.alert-info} +You need to wait for the end of the initialization before accessing `WA.player.tags` + +```typescript +WA.onInit().then(() => { + console.log('Tags: ', WA.player.tags); +}) +``` + ### Listen to player movement ``` WA.player.onPlayerMove(callback: HasPlayerMovedEventCallback): void; @@ -18,4 +75,4 @@ The event has the following attributes : Example : ```javascript WA.player.onPlayerMove(console.log); -``` \ No newline at end of file +``` diff --git a/docs/maps/api-room.md b/docs/maps/api-room.md index a307b2da..ad79f246 100644 --- a/docs/maps/api-room.md +++ b/docs/maps/api-room.md @@ -75,44 +75,58 @@ Example : WA.room.setProperty('wikiLayer', 'openWebsite', 'https://www.wikipedia.org/'); ``` -### Getting information on the current room -``` -WA.room.getCurrentRoom(): Promise -``` -Return a promise that resolves to a `Room` object with the following attributes : -* **id (string) :** ID of the current room -* **map (ITiledMap) :** contains the JSON map file with the properties that were set by the script if `setProperty` was called. -* **mapUrl (string) :** Url of the JSON map file -* **startLayer (string | null) :** Name of the layer where the current user started, only if different from `start` layer +### Get the room id -Example : -```javascript -WA.room.getCurrentRoom((room) => { - if (room.id === '42') { - console.log(room.map); - window.open(room.mapUrl, '_blank'); - } +``` +WA.room.id: string; +``` + +The ID of the current room is available from the `WA.room.id` property. + +{.alert.alert-info} +You need to wait for the end of the initialization before accessing `WA.room.id` + +```typescript +WA.onInit().then(() => { + console.log('Room id: ', WA.room.id); + // Will output something like: '/@/myorg/myworld/myroom', or '/_/global/mymap.org/map.json" }) ``` -### Getting information on the current user -``` -WA.player.getCurrentUser(): Promise -``` -Return a promise that resolves to a `User` object with the following attributes : -* **id (string) :** ID of the current user -* **nickName (string) :** name displayed above the current user -* **tags (string[]) :** list of all the tags of the current user +### Get the map URL -Example : -```javascript -WA.room.getCurrentUser().then((user) => { - if (user.nickName === 'ABC') { - console.log(user.tags); - } +``` +WA.room.mapURL: string; +``` + +The URL of the map is available from the `WA.room.mapURL` property. + +{.alert.alert-info} +You need to wait for the end of the initialization before accessing `WA.room.mapURL` + +```typescript +WA.onInit().then(() => { + console.log('Map URL: ', WA.room.mapURL); + // Will output something like: 'https://mymap.org/map.json" }) ``` + + +### Getting map data +``` +WA.room.getMap(): Promise +``` + +Returns a promise that resolves to the JSON map file. + +```javascript +const map = await WA.room.getMap(); +console.log("Map generated with Tiled version ", map.tiledversion); +``` + +Check the [Tiled documentation to learn more about the format of the JSON map](https://doc.mapeditor.org/en/stable/reference/json-map-format/). + ### Changing tiles ``` WA.room.setTiles(tiles: TileDescriptor[]): void diff --git a/front/src/Api/Events/GameStateEvent.ts b/front/src/Api/Events/GameStateEvent.ts index edeeef80..112c2880 100644 --- a/front/src/Api/Events/GameStateEvent.ts +++ b/front/src/Api/Events/GameStateEvent.ts @@ -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(); /** diff --git a/front/src/Api/Events/IframeEvent.ts b/front/src/Api/Events/IframeEvent.ts index 83d0e12e..613ae525 100644 --- a/front/src/Api/Events/IframeEvent.ts +++ b/front/src/Api/Events/IframeEvent.ts @@ -9,7 +9,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"; @@ -19,8 +19,6 @@ 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 extends MessageEvent { data: T; @@ -46,7 +44,6 @@ export type IframeEventMap = { showLayer: LayerEvent; hideLayer: LayerEvent; setProperty: SetPropertyEvent; - getDataLayer: undefined; loadSound: LoadSoundEvent; playSound: PlaySoundEvent; stopSound: null; @@ -54,8 +51,6 @@ export type IframeEventMap = { registerMenuCommand: MenuItemRegisterEvent; setTiles: SetTilesEvent; setVariable: SetVariableEvent; - // A script/iframe is ready to receive events - ready: null; }; export interface IframeEvent { type: T; @@ -72,10 +67,8 @@ export interface IframeResponseEventMap { leaveEvent: EnterLeaveEvent; buttonClickedEvent: ButtonClickedEvent; hasPlayerMoved: HasPlayerMovedEvent; - dataLayer: DataLayerEvent; menuItemClicked: MenuItemClickedEvent; setVariable: SetVariableEvent; - init: InitEvent; } export interface IframeResponseEvent { type: T; @@ -94,8 +87,14 @@ export const isIframeResponseEventWrapper = (event: { export type IframeQueryMap = { getState: { query: undefined, - answer: GameStateEvent + answer: GameStateEvent, + callback: () => GameStateEvent|PromiseLike }, + getMapData: { + query: undefined, + answer: MapDataEvent, + callback: () => MapDataEvent|PromiseLike + } } export interface IframeQuery { diff --git a/front/src/Api/Events/InitEvent.ts b/front/src/Api/Events/InitEvent.ts deleted file mode 100644 index 47326f81..00000000 --- a/front/src/Api/Events/InitEvent.ts +++ /dev/null @@ -1,10 +0,0 @@ -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; diff --git a/front/src/Api/Events/DataLayerEvent.ts b/front/src/Api/Events/MapDataEvent.ts similarity index 70% rename from front/src/Api/Events/DataLayerEvent.ts rename to front/src/Api/Events/MapDataEvent.ts index 3062c1bc..f63164ed 100644 --- a/front/src/Api/Events/DataLayerEvent.ts +++ b/front/src/Api/Events/MapDataEvent.ts @@ -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; +export type MapDataEvent = tg.GuardedType; diff --git a/front/src/Api/IframeListener.ts b/front/src/Api/IframeListener.ts index 6caecc1f..c74e68a7 100644 --- a/front/src/Api/IframeListener.ts +++ b/front/src/Api/IframeListener.ts @@ -26,7 +26,7 @@ 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"; @@ -34,8 +34,6 @@ import { handleMenuItemRegistrationEvent, isMenuItemRegisterIframeEvent } from " import { SetTilesEvent, isSetTilesEvent } from "./Events/SetTilesEvent"; import { isSetVariableIframeEvent, SetVariableEvent } from "./Events/SetVariableEvent"; -type AnswererCallback = (query: IframeQueryMap[T]['query']) => IframeQueryMap[T]['answer']|Promise; - /** * Listens to messages from iframes and turn those messages into easy to use observables. * Also allows to send messages to those iframes. @@ -89,9 +87,6 @@ class IframeListener { private readonly _setPropertyStream: Subject = new Subject(); public readonly setPropertyStream = this._setPropertyStream.asObservable(); - private readonly _dataLayerChangeStream: Subject = new Subject(); - public readonly dataLayerChangeStream = this._dataLayerChangeStream.asObservable(); - private readonly _registerMenuCommandStream: Subject = new Subject(); public readonly registerMenuCommandStream = this._registerMenuCommandStream.asObservable(); @@ -118,9 +113,14 @@ class IframeListener { private readonly scripts = new Map(); private sendPlayerMove: boolean = false; - private answerers: { - [key in keyof IframeQueryMap]?: AnswererCallback - } = {}; + + // Note: we are forced to type this in "any" because of https://github.com/microsoft/TypeScript/issues/31904 + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private answerers: any = {}; + /*private answerers: { + [key in keyof IframeQueryMap]?: (query: IframeQueryMap[key]['query']) => IframeQueryMap[key]['answer']|PromiseLike + } = {};*/ + init() { window.addEventListener( @@ -194,9 +194,7 @@ class IframeListener { }); } else if (isIframeEventWrapper(payload)) { - if (payload.type === 'ready') { - this._readyStream.next(); - } else if (payload.type === "showLayer" && isLayerEvent(payload.data)) { + 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); @@ -239,8 +237,6 @@ class IframeListener { 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 @@ -269,13 +265,6 @@ class IframeListener { ); } - sendDataLayerEvent(dataLayerEvent: DataLayerEvent) { - this.postMessage({ - type: "dataLayer", - data: dataLayerEvent, - }); - } - /** * Allows the passed iFrame to send/receive messages via the API. */ @@ -439,7 +428,7 @@ class IframeListener { * @param key The "type" of the query we are answering * @param callback */ - public registerAnswerer(key: T, callback: (query: IframeQueryMap[T]['query']) => IframeQueryMap[T]['answer']|Promise ): void { + public registerAnswerer(key: T, callback: (query: IframeQueryMap[T]['query']) => IframeQueryMap[T]['answer']|PromiseLike ): void { this.answerers[key] = callback; } diff --git a/front/src/Api/iframe/player.ts b/front/src/Api/iframe/player.ts index e130d3f2..420e2d18 100644 --- a/front/src/Api/iframe/player.ts +++ b/front/src/Api/iframe/player.ts @@ -6,6 +6,24 @@ import { isHasPlayerMovedEvent } from "../Events/HasPlayerMovedEvent"; const moveStream = new Subject(); +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 { callbacks = [ apiCallback({ @@ -24,6 +42,29 @@ export class WorkadventurePlayerCommands extends IframeApiContribution> = new Map>(); const leaveStreams: Map> = new Map>(); -const dataLayerResolver = new Subject(); -const stateResolvers = new Subject(); const setVariableResolvers = new Subject(); const variables = new Map(); const variableSubscribers = new Map>(); -let immutableDataPromise: Promise | undefined = undefined; - -interface Room { - id: string; - mapUrl: string; - map: ITiledMap; - startLayer: string | null; -} - -interface User { - id: string | undefined; - nickName: string | null; - tags: string[]; -} - interface TileDescriptor { x: number; y: number; @@ -44,18 +27,16 @@ interface TileDescriptor { layer: string; } -function getGameState(): Promise { - if (immutableDataPromise === undefined) { - immutableDataPromise = queryWorkadventure({ type: "getState", data: undefined }); - } - return immutableDataPromise; +let roomId: string|undefined; + +export const setRoomId = (id: string) => { + roomId = id; } -function getDataLayer(): Promise { - return new Promise((resolver, thrower) => { - dataLayerResolver.subscribe(resolver); - sendToWorkadventure({ type: "getDataLayer", data: null }); - }); +let mapURL: string|undefined; + +export const setMapURL = (url: string) => { + mapURL = url; } setVariableResolvers.subscribe((event) => { @@ -82,13 +63,6 @@ export class WorkadventureRoomCommands extends IframeApiContribution { - dataLayerResolver.next(payloadData); - }, - }), apiCallback({ type: "setVariable", typeChecker: isSetVariableEvent, @@ -130,22 +104,9 @@ export class WorkadventureRoomCommands extends IframeApiContribution { - return getGameState().then((gameState) => { - return getDataLayer().then((mapJson) => { - return { - id: gameState.roomId, - map: mapJson.data as ITiledMap, - mapUrl: gameState.mapUrl, - startLayer: gameState.startLayerName, - }; - }); - }); - } - getCurrentUser(): Promise { - return getGameState().then((gameState) => { - return { id: gameState.uuid, nickName: gameState.nickname, tags: gameState.tags }; - }); + async getMap(): Promise { + const event = await queryWorkadventure({ type: "getMapData", data: undefined }); + return event.data as ITiledMap; } setTiles(tiles: TileDescriptor[]) { sendToWorkadventure({ @@ -177,6 +138,21 @@ export class WorkadventureRoomCommands extends IframeApiContribution { - 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) { @@ -1061,20 +1043,24 @@ ${escapedMessage} }) ); - this.iframeSubscriptionList.push( - iframeListener.dataLayerChangeStream.subscribe(() => { - iframeListener.sendDataLayerEvent({ data: this.gameMap.getMap() }); - }) - ); + iframeListener.registerAnswerer('getMapData', () => { + return { + data: this.gameMap.getMap() + } + }); - iframeListener.registerAnswerer('getState', () => { + iframeListener.registerAnswerer('getState', async () => { + // The sharedVariablesManager is not instantiated before the connection is established. So we need to wait + // for the connection to send back the answer. + await this.connectionAnswerPromise; return { mapUrl: this.MapUrlFile, startLayerName: this.startPositionCalculator.startLayerName, uuid: localUserStore.getLocalUser()?.uuid, - nickname: localUserStore.getName(), + nickname: this.playerName, roomId: this.RoomId, tags: this.connection ? this.connection.getAllTags() : [], + variables: this.sharedVariablesManager.variables, }; }); this.iframeSubscriptionList.push( diff --git a/front/src/iframe_api.ts b/front/src/iframe_api.ts index b27bda2d..06e3ff7e 100644 --- a/front/src/iframe_api.ts +++ b/front/src/iframe_api.ts @@ -11,12 +11,12 @@ import nav from "./Api/iframe/nav"; import controls from "./Api/iframe/controls"; import ui from "./Api/iframe/ui"; import sound from "./Api/iframe/sound"; -import room from "./Api/iframe/room"; -import player from "./Api/iframe/player"; +import room, {setMapURL, setRoomId} from "./Api/iframe/room"; +import player, {setPlayerName, setTags, setUuid} from "./Api/iframe/player"; import type { ButtonDescriptor } from "./Api/iframe/Ui/ButtonDescriptor"; import type { Popup } from "./Api/iframe/Ui/Popup"; import type { Sound } from "./Api/iframe/Sound/Sound"; -import { answerPromises, sendToWorkadventure} from "./Api/iframe/IframeApiContribution"; +import {answerPromises, queryWorkadventure, sendToWorkadventure} from "./Api/iframe/IframeApiContribution"; const wa = { ui, @@ -208,7 +208,15 @@ window.addEventListener( ); // Notify WorkAdventure that we are ready to receive data -sendToWorkadventure({ - type: 'ready', - data: null -}); +queryWorkadventure({ + type: 'getState', + data: undefined +}).then((state => { + setPlayerName(state.nickname); + setRoomId(state.roomId); + setMapURL(state.mapUrl); + setTags(state.tags); + setUuid(state.uuid); + + // TODO: remove the WA.room.getRoom method and replace it with WA.room.getMapData and WA.player.nickname, etc... +}));