Migrating WA.player.getCurrentUser and WA.room.getCurrentRoom to direct property access and WA.room.getMap

This commit is contained in:
David Négrier 2021-07-05 11:53:33 +02:00
parent ea1460abaf
commit 62a4814961
11 changed files with 220 additions and 159 deletions

View file

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

View file

@ -75,44 +75,58 @@ Example :
WA.room.setProperty('wikiLayer', 'openWebsite', 'https://www.wikipedia.org/');
```
### Getting information on the current room
```
WA.room.getCurrentRoom(): Promise<Room>
```
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<User>
```
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<ITiledMap>
```
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

View file

@ -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();
/**

View file

@ -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<T> 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<T extends keyof IframeEventMap> {
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<T extends keyof IframeResponseEventMap> {
type: T;
@ -94,8 +87,14 @@ export const isIframeResponseEventWrapper = (event: {
export type IframeQueryMap = {
getState: {
query: undefined,
answer: GameStateEvent
answer: GameStateEvent,
callback: () => GameStateEvent|PromiseLike<GameStateEvent>
},
getMapData: {
query: undefined,
answer: MapDataEvent,
callback: () => MapDataEvent|PromiseLike<GameStateEvent>
}
}
export interface IframeQuery<T extends keyof IframeQueryMap> {

View file

@ -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<typeof isInitEvent>;

View file

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

View file

@ -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<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.
@ -89,9 +87,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();
@ -118,9 +113,14 @@ class IframeListener {
private readonly scripts = new Map<string, HTMLIFrameElement>();
private sendPlayerMove: boolean = false;
private answerers: {
[key in keyof IframeQueryMap]?: AnswererCallback<key>
} = {};
// 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<IframeQueryMap[key]['answer']>
} = {};*/
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<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: (query: IframeQueryMap[T]['query']) => IframeQueryMap[T]['answer']|PromiseLike<IframeQueryMap[T]['answer']> ): void {
this.answerers[key] = callback;
}

View file

@ -6,6 +6,24 @@ import { isHasPlayerMovedEvent } from "../Events/HasPlayerMovedEvent";
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({
@ -24,6 +42,29 @@ export class WorkadventurePlayerCommands extends IframeApiContribution<Workadven
data: null,
});
}
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;
}
}
export default new WorkadventurePlayerCommands();

View file

@ -1,6 +1,6 @@
import {Observable, Subject} from "rxjs";
import { isDataLayerEvent } from "../Events/DataLayerEvent";
import { isMapDataEvent } from "../Events/MapDataEvent";
import { EnterLeaveEvent, isEnterLeaveEvent } from "../Events/EnterLeaveEvent";
import { isGameStateEvent } from "../Events/GameStateEvent";
@ -11,32 +11,15 @@ import type {SetPropertyEvent} from "../Events/setPropertyEvent";
import {isSetVariableEvent, SetVariableEvent} from "../Events/SetVariableEvent";
import type { ITiledMap } from "../../Phaser/Map/ITiledMap";
import type { DataLayerEvent } from "../Events/DataLayerEvent";
import type { MapDataEvent } from "../Events/MapDataEvent";
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>();
const setVariableResolvers = new Subject<SetVariableEvent>();
const variables = new Map<string, unknown>();
const variableSubscribers = new Map<string, Subject<unknown>>();
let immutableDataPromise: Promise<GameStateEvent> | 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<GameStateEvent> {
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<DataLayerEvent> {
return new Promise<DataLayerEvent>((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<Workadventu
leaveStreams.get(payloadData.name)?.next();
},
}),
apiCallback({
type: "dataLayer",
typeChecker: isDataLayerEvent,
callback: (payloadData) => {
dataLayerResolver.next(payloadData);
},
}),
apiCallback({
type: "setVariable",
typeChecker: isSetVariableEvent,
@ -130,22 +104,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,
};
});
});
}
getCurrentUser(): Promise<User> {
return getGameState().then((gameState) => {
return { id: gameState.uuid, nickName: gameState.nickname, tags: gameState.tags };
});
async getMap(): Promise<ITiledMap> {
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<Workadventu
}
return subject.asObservable();
}
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();

View file

@ -92,7 +92,6 @@ import { peerStore, screenSharingPeerStore } from "../../Stores/PeerStore";
import { videoFocusStore } from "../../Stores/VideoFocusStore";
import { biggestAvailableAreaStore } from "../../Stores/BiggestAvailableAreaStore";
import { SharedVariablesManager } from "./SharedVariablesManager";
import type {InitEvent} from "../../Api/Events/InitEvent";
export interface GameSceneInitInterface {
initPosition: PointInterface | null;
@ -399,23 +398,6 @@ export class GameScene extends DirtyScene {
});
}
this.iframeSubscriptionList.push(iframeListener.readyStream.subscribe((iframe) => {
this.connectionAnswerPromise.then(connection => {
// Generate init message for an iframe
// TODO: merge with GameStateEvent
const initEvent: InitEvent = {
variables: this.sharedVariablesManager.variables
}
});
// TODO: SEND INIT MESSAGE TO IFRAMES ONLY WHEN CONNECTION IS ESTABLISHED
// TODO: SEND INIT MESSAGE TO IFRAMES ONLY WHEN CONNECTION IS ESTABLISHED
// TODO: SEND INIT MESSAGE TO IFRAMES ONLY WHEN CONNECTION IS ESTABLISHED
// TODO: SEND INIT MESSAGE TO IFRAMES ONLY WHEN CONNECTION IS ESTABLISHED
// TODO: SEND INIT MESSAGE TO IFRAMES ONLY WHEN CONNECTION IS ESTABLISHED
}));
// Now, let's load the script, if any
const scripts = this.getScriptUrls(this.mapFile);
for (const script of scripts) {
@ -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(

View file

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