Merge pull request #1239 from thecodingmachine/scripting_api_room_metadata
Allowing loading/saving "metadata" from a room
This commit is contained in:
commit
9b2914cc63
16
CHANGELOG.md
16
CHANGELOG.md
@ -8,14 +8,24 @@
|
|||||||
- Migrated the admin console to Svelte, and redesigned the console #1211
|
- Migrated the admin console to Svelte, and redesigned the console #1211
|
||||||
- Layer properties (like `exitUrl`, `silent`, etc...) can now also used in tile properties #1210 (@jonnytest1)
|
- Layer properties (like `exitUrl`, `silent`, etc...) can now also used in tile properties #1210 (@jonnytest1)
|
||||||
- New scripting API features :
|
- New scripting API features :
|
||||||
|
- Use `WA.onInit(): Promise<void>` to wait for scripting API initialization
|
||||||
- Use `WA.room.showLayer(): void` to show a layer
|
- Use `WA.room.showLayer(): void` to show a layer
|
||||||
- Use `WA.room.hideLayer(): void` to hide a layer
|
- Use `WA.room.hideLayer(): void` to hide a layer
|
||||||
- Use `WA.room.setProperty() : void` to add, delete or change existing property of a layer
|
- Use `WA.room.setProperty() : void` to add, delete or change existing property of a layer
|
||||||
- Use `WA.player.onPlayerMove(): void` to track the movement of the current player
|
- Use `WA.player.onPlayerMove(): void` to track the movement of the current player
|
||||||
- Use `WA.player.getCurrentUser(): Promise<User>` to get the ID, name and tags of the current player
|
- Use `WA.player.id: string|undefined` to get the ID of the current player
|
||||||
- Use `WA.room.getCurrentRoom(): Promise<Room>` to get the ID, JSON map file, url of the map of the current room and the layer where the current player started
|
- Use `WA.player.name: string` to get the name of the current player
|
||||||
- Use `WA.ui.registerMenuCommand(): void` to add a custom menu
|
- Use `WA.player.tags: string[]` to get the tags of the current player
|
||||||
|
- Use `WA.room.id: string` to get the ID of the room
|
||||||
|
- Use `WA.room.mapURL: string` to get the URL of the map
|
||||||
|
- Use `WA.room.mapURL: string` to get the URL of the map
|
||||||
|
- Use `WA.room.getMap(): Promise<ITiledMap>` to get the JSON map file
|
||||||
- Use `WA.room.setTiles(): void` to add, delete or change an array of tiles
|
- Use `WA.room.setTiles(): void` to add, delete or change an array of tiles
|
||||||
|
- Use `WA.ui.registerMenuCommand(): void` to add a custom menu
|
||||||
|
- Use `WA.state.loadVariable(key: string): unknown` to retrieve a variable
|
||||||
|
- Use `WA.state.saveVariable(key: string, value: unknown): Promise<void>` to set a variable (across the room, for all users)
|
||||||
|
- Use `WA.state.onVariableChange(key: string): Observable<unknown>` to track a variable
|
||||||
|
- Use `WA.state.[any variable]: unknown` to access directly any variable (this is a shortcut to using `WA.state.loadVariable` and `WA.state.saveVariable`)
|
||||||
- Users blocking now relies on UUID rather than ID. A blocked user that leaves a room and comes back will stay blocked.
|
- Users blocking now relies on UUID rather than ID. A blocked user that leaves a room and comes back will stay blocked.
|
||||||
- The text chat was redesigned to be prettier and to use more features :
|
- The text chat was redesigned to be prettier and to use more features :
|
||||||
- The chat is now persistent bewteen discussions and always accesible
|
- The chat is now persistent bewteen discussions and always accesible
|
||||||
|
@ -40,6 +40,7 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://github.com/thecodingmachine/workadventure#readme",
|
"homepage": "https://github.com/thecodingmachine/workadventure#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@workadventure/tiled-map-type-guard": "^1.0.0",
|
||||||
"axios": "^0.21.1",
|
"axios": "^0.21.1",
|
||||||
"busboy": "^0.3.1",
|
"busboy": "^0.3.1",
|
||||||
"circular-json": "^0.5.9",
|
"circular-json": "^0.5.9",
|
||||||
@ -47,10 +48,12 @@
|
|||||||
"generic-type-guard": "^3.2.0",
|
"generic-type-guard": "^3.2.0",
|
||||||
"google-protobuf": "^3.13.0",
|
"google-protobuf": "^3.13.0",
|
||||||
"grpc": "^1.24.4",
|
"grpc": "^1.24.4",
|
||||||
|
"ipaddr.js": "^2.0.1",
|
||||||
"jsonwebtoken": "^8.5.1",
|
"jsonwebtoken": "^8.5.1",
|
||||||
"mkdirp": "^1.0.4",
|
"mkdirp": "^1.0.4",
|
||||||
"prom-client": "^12.0.0",
|
"prom-client": "^12.0.0",
|
||||||
"query-string": "^6.13.3",
|
"query-string": "^6.13.3",
|
||||||
|
"redis": "^3.1.2",
|
||||||
"systeminformation": "^4.31.1",
|
"systeminformation": "^4.31.1",
|
||||||
"uWebSockets.js": "uNetworking/uWebSockets.js#v18.5.0",
|
"uWebSockets.js": "uNetworking/uWebSockets.js#v18.5.0",
|
||||||
"uuidv4": "^6.0.7"
|
"uuidv4": "^6.0.7"
|
||||||
@ -64,6 +67,7 @@
|
|||||||
"@types/jasmine": "^3.5.10",
|
"@types/jasmine": "^3.5.10",
|
||||||
"@types/jsonwebtoken": "^8.3.8",
|
"@types/jsonwebtoken": "^8.3.8",
|
||||||
"@types/mkdirp": "^1.0.1",
|
"@types/mkdirp": "^1.0.1",
|
||||||
|
"@types/redis": "^2.8.31",
|
||||||
"@types/uuidv4": "^5.0.0",
|
"@types/uuidv4": "^5.0.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^2.26.0",
|
"@typescript-eslint/eslint-plugin": "^2.26.0",
|
||||||
"@typescript-eslint/parser": "^2.26.0",
|
"@typescript-eslint/parser": "^2.26.0",
|
||||||
|
@ -12,6 +12,9 @@ const GRPC_PORT = parseInt(process.env.GRPC_PORT || "50051") || 50051;
|
|||||||
export const SOCKET_IDLE_TIMER = parseInt(process.env.SOCKET_IDLE_TIMER as string) || 30; // maximum time (in second) without activity before a socket is closed
|
export const SOCKET_IDLE_TIMER = parseInt(process.env.SOCKET_IDLE_TIMER as string) || 30; // maximum time (in second) without activity before a socket is closed
|
||||||
export const TURN_STATIC_AUTH_SECRET = process.env.TURN_STATIC_AUTH_SECRET || "";
|
export const TURN_STATIC_AUTH_SECRET = process.env.TURN_STATIC_AUTH_SECRET || "";
|
||||||
export const MAX_PER_GROUP = parseInt(process.env.MAX_PER_GROUP || "4");
|
export const MAX_PER_GROUP = parseInt(process.env.MAX_PER_GROUP || "4");
|
||||||
|
export const REDIS_HOST = process.env.REDIS_HOST || undefined;
|
||||||
|
export const REDIS_PORT = parseInt(process.env.REDIS_PORT || "6379") || 6379;
|
||||||
|
export const REDIS_PASSWORD = process.env.REDIS_PASSWORD || undefined;
|
||||||
|
|
||||||
export {
|
export {
|
||||||
MINIMUM_DISTANCE,
|
MINIMUM_DISTANCE,
|
||||||
|
@ -5,35 +5,63 @@ import { PositionInterface } from "_Model/PositionInterface";
|
|||||||
import { EmoteCallback, EntersCallback, LeavesCallback, MovesCallback } from "_Model/Zone";
|
import { EmoteCallback, EntersCallback, LeavesCallback, MovesCallback } from "_Model/Zone";
|
||||||
import { PositionNotifier } from "./PositionNotifier";
|
import { PositionNotifier } from "./PositionNotifier";
|
||||||
import { Movable } from "_Model/Movable";
|
import { Movable } from "_Model/Movable";
|
||||||
import { EmoteEventMessage, JoinRoomMessage } from "../Messages/generated/messages_pb";
|
import {
|
||||||
|
BatchToPusherMessage,
|
||||||
|
BatchToPusherRoomMessage,
|
||||||
|
EmoteEventMessage,
|
||||||
|
ErrorMessage,
|
||||||
|
JoinRoomMessage,
|
||||||
|
SubToPusherRoomMessage,
|
||||||
|
VariableMessage,
|
||||||
|
VariableWithTagMessage,
|
||||||
|
} from "../Messages/generated/messages_pb";
|
||||||
import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils";
|
import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils";
|
||||||
import { ZoneSocket } from "src/RoomManager";
|
import { RoomSocket, ZoneSocket } from "src/RoomManager";
|
||||||
import { Admin } from "../Model/Admin";
|
import { Admin } from "../Model/Admin";
|
||||||
|
import { adminApi } from "../Services/AdminApi";
|
||||||
|
import { isMapDetailsData, MapDetailsData } from "../Services/AdminApi/MapDetailsData";
|
||||||
|
import { ITiledMap } from "@workadventure/tiled-map-type-guard/dist";
|
||||||
|
import { mapFetcher } from "../Services/MapFetcher";
|
||||||
|
import { VariablesManager } from "../Services/VariablesManager";
|
||||||
|
import { ADMIN_API_URL } from "../Enum/EnvironmentVariable";
|
||||||
|
import { LocalUrlError } from "../Services/LocalUrlError";
|
||||||
|
import { emitErrorOnRoomSocket } from "../Services/MessageHelpers";
|
||||||
|
|
||||||
export type ConnectCallback = (user: User, group: Group) => void;
|
export type ConnectCallback = (user: User, group: Group) => void;
|
||||||
export type DisconnectCallback = (user: User, group: Group) => void;
|
export type DisconnectCallback = (user: User, group: Group) => void;
|
||||||
|
|
||||||
export class GameRoom {
|
export class GameRoom {
|
||||||
private readonly minDistance: number;
|
|
||||||
private readonly groupRadius: number;
|
|
||||||
|
|
||||||
// Users, sorted by ID
|
// Users, sorted by ID
|
||||||
private readonly users: Map<number, User>;
|
private readonly users = new Map<number, User>();
|
||||||
private readonly usersByUuid: Map<string, User>;
|
private readonly usersByUuid = new Map<string, User>();
|
||||||
private readonly groups: Set<Group>;
|
private readonly groups = new Set<Group>();
|
||||||
private readonly admins: Set<Admin>;
|
private readonly admins = new Set<Admin>();
|
||||||
|
|
||||||
private readonly connectCallback: ConnectCallback;
|
private itemsState = new Map<number, unknown>();
|
||||||
private readonly disconnectCallback: DisconnectCallback;
|
|
||||||
|
|
||||||
private itemsState: Map<number, unknown> = new Map<number, unknown>();
|
|
||||||
|
|
||||||
private readonly positionNotifier: PositionNotifier;
|
private readonly positionNotifier: PositionNotifier;
|
||||||
public readonly roomUrl: string;
|
|
||||||
private versionNumber: number = 1;
|
private versionNumber: number = 1;
|
||||||
private nextUserId: number = 1;
|
private nextUserId: number = 1;
|
||||||
|
|
||||||
constructor(
|
private roomListeners: Set<RoomSocket> = new Set<RoomSocket>();
|
||||||
|
|
||||||
|
private constructor(
|
||||||
|
public readonly roomUrl: string,
|
||||||
|
private mapUrl: string,
|
||||||
|
private readonly connectCallback: ConnectCallback,
|
||||||
|
private readonly disconnectCallback: DisconnectCallback,
|
||||||
|
private readonly minDistance: number,
|
||||||
|
private readonly groupRadius: number,
|
||||||
|
onEnters: EntersCallback,
|
||||||
|
onMoves: MovesCallback,
|
||||||
|
onLeaves: LeavesCallback,
|
||||||
|
onEmote: EmoteCallback
|
||||||
|
) {
|
||||||
|
// A zone is 10 sprites wide.
|
||||||
|
this.positionNotifier = new PositionNotifier(320, 320, onEnters, onMoves, onLeaves, onEmote);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async create(
|
||||||
roomUrl: string,
|
roomUrl: string,
|
||||||
connectCallback: ConnectCallback,
|
connectCallback: ConnectCallback,
|
||||||
disconnectCallback: DisconnectCallback,
|
disconnectCallback: DisconnectCallback,
|
||||||
@ -43,19 +71,23 @@ export class GameRoom {
|
|||||||
onMoves: MovesCallback,
|
onMoves: MovesCallback,
|
||||||
onLeaves: LeavesCallback,
|
onLeaves: LeavesCallback,
|
||||||
onEmote: EmoteCallback
|
onEmote: EmoteCallback
|
||||||
) {
|
): Promise<GameRoom> {
|
||||||
this.roomUrl = roomUrl;
|
const mapDetails = await GameRoom.getMapDetails(roomUrl);
|
||||||
|
|
||||||
this.users = new Map<number, User>();
|
const gameRoom = new GameRoom(
|
||||||
this.usersByUuid = new Map<string, User>();
|
roomUrl,
|
||||||
this.admins = new Set<Admin>();
|
mapDetails.mapUrl,
|
||||||
this.groups = new Set<Group>();
|
connectCallback,
|
||||||
this.connectCallback = connectCallback;
|
disconnectCallback,
|
||||||
this.disconnectCallback = disconnectCallback;
|
minDistance,
|
||||||
this.minDistance = minDistance;
|
groupRadius,
|
||||||
this.groupRadius = groupRadius;
|
onEnters,
|
||||||
// A zone is 10 sprites wide.
|
onMoves,
|
||||||
this.positionNotifier = new PositionNotifier(320, 320, onEnters, onMoves, onLeaves, onEmote);
|
onLeaves,
|
||||||
|
onEmote
|
||||||
|
);
|
||||||
|
|
||||||
|
return gameRoom;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getGroups(): Group[] {
|
public getGroups(): Group[] {
|
||||||
@ -289,6 +321,32 @@ export class GameRoom {
|
|||||||
return this.itemsState;
|
return this.itemsState;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async setVariable(name: string, value: string, user: User): Promise<void> {
|
||||||
|
// First, let's check if "user" is allowed to modify the variable.
|
||||||
|
const variableManager = await this.getVariableManager();
|
||||||
|
|
||||||
|
const readableBy = variableManager.setVariable(name, value, user);
|
||||||
|
|
||||||
|
// TODO: should we batch those every 100ms?
|
||||||
|
const variableMessage = new VariableWithTagMessage();
|
||||||
|
variableMessage.setName(name);
|
||||||
|
variableMessage.setValue(value);
|
||||||
|
if (readableBy) {
|
||||||
|
variableMessage.setReadableby(readableBy);
|
||||||
|
}
|
||||||
|
|
||||||
|
const subMessage = new SubToPusherRoomMessage();
|
||||||
|
subMessage.setVariablemessage(variableMessage);
|
||||||
|
|
||||||
|
const batchMessage = new BatchToPusherRoomMessage();
|
||||||
|
batchMessage.addPayload(subMessage);
|
||||||
|
|
||||||
|
// Dispatch the message on the room listeners
|
||||||
|
for (const socket of this.roomListeners) {
|
||||||
|
socket.write(batchMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public addZoneListener(call: ZoneSocket, x: number, y: number): Set<Movable> {
|
public addZoneListener(call: ZoneSocket, x: number, y: number): Set<Movable> {
|
||||||
return this.positionNotifier.addZoneListener(call, x, y);
|
return this.positionNotifier.addZoneListener(call, x, y);
|
||||||
}
|
}
|
||||||
@ -318,4 +376,98 @@ export class GameRoom {
|
|||||||
public emitEmoteEvent(user: User, emoteEventMessage: EmoteEventMessage) {
|
public emitEmoteEvent(user: User, emoteEventMessage: EmoteEventMessage) {
|
||||||
this.positionNotifier.emitEmoteEvent(user, emoteEventMessage);
|
this.positionNotifier.emitEmoteEvent(user, emoteEventMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public addRoomListener(socket: RoomSocket) {
|
||||||
|
this.roomListeners.add(socket);
|
||||||
|
}
|
||||||
|
|
||||||
|
public removeRoomListener(socket: RoomSocket) {
|
||||||
|
this.roomListeners.delete(socket);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Connects to the admin server to fetch map details.
|
||||||
|
* If there is no admin server, the map details are generated by analysing the map URL (that must be in the form: /_/instance/map_url)
|
||||||
|
*/
|
||||||
|
private static async getMapDetails(roomUrl: string): Promise<MapDetailsData> {
|
||||||
|
if (!ADMIN_API_URL) {
|
||||||
|
const roomUrlObj = new URL(roomUrl);
|
||||||
|
|
||||||
|
const match = /\/_\/[^/]+\/(.+)/.exec(roomUrlObj.pathname);
|
||||||
|
if (!match) {
|
||||||
|
console.error("Unexpected room URL", roomUrl);
|
||||||
|
throw new Error('Unexpected room URL "' + roomUrl + '"');
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapUrl = roomUrlObj.protocol + "//" + match[1];
|
||||||
|
|
||||||
|
return {
|
||||||
|
mapUrl,
|
||||||
|
policy_type: 1,
|
||||||
|
textures: [],
|
||||||
|
tags: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await adminApi.fetchMapDetails(roomUrl);
|
||||||
|
if (!isMapDetailsData(result)) {
|
||||||
|
console.error("Unexpected room details received from server", result);
|
||||||
|
throw new Error("Unexpected room details received from server");
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private mapPromise: Promise<ITiledMap> | undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a promise to the map file.
|
||||||
|
* @throws LocalUrlError if the map we are trying to load is hosted on a local network
|
||||||
|
* @throws Error
|
||||||
|
*/
|
||||||
|
private getMap(): Promise<ITiledMap> {
|
||||||
|
if (!this.mapPromise) {
|
||||||
|
this.mapPromise = mapFetcher.fetchMap(this.mapUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.mapPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
private variableManagerPromise: Promise<VariablesManager> | undefined;
|
||||||
|
|
||||||
|
private getVariableManager(): Promise<VariablesManager> {
|
||||||
|
if (!this.variableManagerPromise) {
|
||||||
|
this.variableManagerPromise = this.getMap()
|
||||||
|
.then((map) => {
|
||||||
|
const variablesManager = new VariablesManager(this.roomUrl, map);
|
||||||
|
return variablesManager.init();
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
if (e instanceof LocalUrlError) {
|
||||||
|
// If we are trying to load a local URL, we are probably in test mode.
|
||||||
|
// In this case, let's bypass the server-side checks completely.
|
||||||
|
|
||||||
|
// Note: we run this message inside a setTimeout so that the room listeners can have time to connect.
|
||||||
|
setTimeout(() => {
|
||||||
|
for (const roomListener of this.roomListeners) {
|
||||||
|
emitErrorOnRoomSocket(
|
||||||
|
roomListener,
|
||||||
|
"You are loading a local map. If you use the scripting API in this map, please be aware that server-side checks and variable persistence is disabled."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
const variablesManager = new VariablesManager(this.roomUrl, null);
|
||||||
|
return variablesManager.init();
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return this.variableManagerPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getVariablesForTags(tags: string[]): Promise<Map<string, string>> {
|
||||||
|
const variablesManager = await this.getVariableManager();
|
||||||
|
return variablesManager.getVariablesForTags(tags);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,8 @@ import {
|
|||||||
AdminPusherToBackMessage,
|
AdminPusherToBackMessage,
|
||||||
AdminRoomMessage,
|
AdminRoomMessage,
|
||||||
BanMessage,
|
BanMessage,
|
||||||
|
BatchToPusherMessage,
|
||||||
|
BatchToPusherRoomMessage,
|
||||||
EmotePromptMessage,
|
EmotePromptMessage,
|
||||||
EmptyMessage,
|
EmptyMessage,
|
||||||
ItemEventMessage,
|
ItemEventMessage,
|
||||||
@ -13,17 +15,18 @@ import {
|
|||||||
PusherToBackMessage,
|
PusherToBackMessage,
|
||||||
QueryJitsiJwtMessage,
|
QueryJitsiJwtMessage,
|
||||||
RefreshRoomPromptMessage,
|
RefreshRoomPromptMessage,
|
||||||
|
RoomMessage,
|
||||||
ServerToAdminClientMessage,
|
ServerToAdminClientMessage,
|
||||||
ServerToClientMessage,
|
|
||||||
SilentMessage,
|
SilentMessage,
|
||||||
UserMovesMessage,
|
UserMovesMessage,
|
||||||
|
VariableMessage,
|
||||||
WebRtcSignalToServerMessage,
|
WebRtcSignalToServerMessage,
|
||||||
WorldFullWarningToRoomMessage,
|
WorldFullWarningToRoomMessage,
|
||||||
ZoneMessage,
|
ZoneMessage,
|
||||||
} from "./Messages/generated/messages_pb";
|
} from "./Messages/generated/messages_pb";
|
||||||
import { sendUnaryData, ServerDuplexStream, ServerUnaryCall, ServerWritableStream } from "grpc";
|
import { sendUnaryData, ServerDuplexStream, ServerUnaryCall, ServerWritableStream } from "grpc";
|
||||||
import { socketManager } from "./Services/SocketManager";
|
import { socketManager } from "./Services/SocketManager";
|
||||||
import { emitError } from "./Services/MessageHelpers";
|
import { emitError, emitErrorOnRoomSocket, emitErrorOnZoneSocket } from "./Services/MessageHelpers";
|
||||||
import { User, UserSocket } from "./Model/User";
|
import { User, UserSocket } from "./Model/User";
|
||||||
import { GameRoom } from "./Model/GameRoom";
|
import { GameRoom } from "./Model/GameRoom";
|
||||||
import Debug from "debug";
|
import Debug from "debug";
|
||||||
@ -32,7 +35,8 @@ import { Admin } from "./Model/Admin";
|
|||||||
const debug = Debug("roommanager");
|
const debug = Debug("roommanager");
|
||||||
|
|
||||||
export type AdminSocket = ServerDuplexStream<AdminPusherToBackMessage, ServerToAdminClientMessage>;
|
export type AdminSocket = ServerDuplexStream<AdminPusherToBackMessage, ServerToAdminClientMessage>;
|
||||||
export type ZoneSocket = ServerWritableStream<ZoneMessage, ServerToClientMessage>;
|
export type ZoneSocket = ServerWritableStream<ZoneMessage, BatchToPusherMessage>;
|
||||||
|
export type RoomSocket = ServerWritableStream<RoomMessage, BatchToPusherRoomMessage>;
|
||||||
|
|
||||||
const roomManager: IRoomManagerServer = {
|
const roomManager: IRoomManagerServer = {
|
||||||
joinRoom: (call: UserSocket): void => {
|
joinRoom: (call: UserSocket): void => {
|
||||||
@ -41,7 +45,7 @@ const roomManager: IRoomManagerServer = {
|
|||||||
let room: GameRoom | null = null;
|
let room: GameRoom | null = null;
|
||||||
let user: User | null = null;
|
let user: User | null = null;
|
||||||
|
|
||||||
call.on("data", (message: PusherToBackMessage) => {
|
call.on("data", async (message: PusherToBackMessage) => {
|
||||||
try {
|
try {
|
||||||
if (room === null || user === null) {
|
if (room === null || user === null) {
|
||||||
if (message.hasJoinroommessage()) {
|
if (message.hasJoinroommessage()) {
|
||||||
@ -55,7 +59,8 @@ const roomManager: IRoomManagerServer = {
|
|||||||
//Connexion may have been closed before the init was finished, so we have to manually disconnect the user.
|
//Connexion may have been closed before the init was finished, so we have to manually disconnect the user.
|
||||||
socketManager.leaveRoom(gameRoom, myUser);
|
socketManager.leaveRoom(gameRoom, myUser);
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
.catch((e) => emitError(call, e));
|
||||||
} else {
|
} else {
|
||||||
throw new Error("The first message sent MUST be of type JoinRoomMessage");
|
throw new Error("The first message sent MUST be of type JoinRoomMessage");
|
||||||
}
|
}
|
||||||
@ -72,6 +77,12 @@ const roomManager: IRoomManagerServer = {
|
|||||||
socketManager.handleSilentMessage(room, user, message.getSilentmessage() as SilentMessage);
|
socketManager.handleSilentMessage(room, user, message.getSilentmessage() as SilentMessage);
|
||||||
} else if (message.hasItemeventmessage()) {
|
} else if (message.hasItemeventmessage()) {
|
||||||
socketManager.handleItemEvent(room, user, message.getItemeventmessage() as ItemEventMessage);
|
socketManager.handleItemEvent(room, user, message.getItemeventmessage() as ItemEventMessage);
|
||||||
|
} else if (message.hasVariablemessage()) {
|
||||||
|
await socketManager.handleVariableEvent(
|
||||||
|
room,
|
||||||
|
user,
|
||||||
|
message.getVariablemessage() as VariableMessage
|
||||||
|
);
|
||||||
} else if (message.hasWebrtcsignaltoservermessage()) {
|
} else if (message.hasWebrtcsignaltoservermessage()) {
|
||||||
socketManager.emitVideo(
|
socketManager.emitVideo(
|
||||||
room,
|
room,
|
||||||
@ -112,6 +123,7 @@ const roomManager: IRoomManagerServer = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
emitError(call, e);
|
emitError(call, e);
|
||||||
call.end();
|
call.end();
|
||||||
}
|
}
|
||||||
@ -136,20 +148,54 @@ const roomManager: IRoomManagerServer = {
|
|||||||
debug("listenZone called");
|
debug("listenZone called");
|
||||||
const zoneMessage = call.request;
|
const zoneMessage = call.request;
|
||||||
|
|
||||||
socketManager.addZoneListener(call, zoneMessage.getRoomid(), zoneMessage.getX(), zoneMessage.getY());
|
socketManager
|
||||||
|
.addZoneListener(call, zoneMessage.getRoomid(), zoneMessage.getX(), zoneMessage.getY())
|
||||||
|
.catch((e) => {
|
||||||
|
emitErrorOnZoneSocket(call, e.toString());
|
||||||
|
});
|
||||||
|
|
||||||
call.on("cancelled", () => {
|
call.on("cancelled", () => {
|
||||||
debug("listenZone cancelled");
|
debug("listenZone cancelled");
|
||||||
socketManager.removeZoneListener(call, zoneMessage.getRoomid(), zoneMessage.getX(), zoneMessage.getY());
|
socketManager
|
||||||
|
.removeZoneListener(call, zoneMessage.getRoomid(), zoneMessage.getX(), zoneMessage.getY())
|
||||||
|
.catch((e) => console.error(e));
|
||||||
call.end();
|
call.end();
|
||||||
});
|
});
|
||||||
|
|
||||||
call.on("close", () => {
|
call.on("close", () => {
|
||||||
debug("listenZone connection closed");
|
debug("listenZone connection closed");
|
||||||
socketManager.removeZoneListener(call, zoneMessage.getRoomid(), zoneMessage.getX(), zoneMessage.getY());
|
socketManager
|
||||||
|
.removeZoneListener(call, zoneMessage.getRoomid(), zoneMessage.getX(), zoneMessage.getY())
|
||||||
|
.catch((e) => console.error(e));
|
||||||
}).on("error", (e) => {
|
}).on("error", (e) => {
|
||||||
console.error("An error occurred in listenZone stream:", e);
|
console.error("An error occurred in listenZone stream:", e);
|
||||||
socketManager.removeZoneListener(call, zoneMessage.getRoomid(), zoneMessage.getX(), zoneMessage.getY());
|
socketManager
|
||||||
|
.removeZoneListener(call, zoneMessage.getRoomid(), zoneMessage.getX(), zoneMessage.getY())
|
||||||
|
.catch((e) => console.error(e));
|
||||||
|
call.end();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
listenRoom(call: RoomSocket): void {
|
||||||
|
debug("listenRoom called");
|
||||||
|
const roomMessage = call.request;
|
||||||
|
|
||||||
|
socketManager.addRoomListener(call, roomMessage.getRoomid()).catch((e) => {
|
||||||
|
emitErrorOnRoomSocket(call, e.toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
call.on("cancelled", () => {
|
||||||
|
debug("listenRoom cancelled");
|
||||||
|
socketManager.removeRoomListener(call, roomMessage.getRoomid()).catch((e) => console.error(e));
|
||||||
|
call.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
call.on("close", () => {
|
||||||
|
debug("listenRoom connection closed");
|
||||||
|
socketManager.removeRoomListener(call, roomMessage.getRoomid()).catch((e) => console.error(e));
|
||||||
|
}).on("error", (e) => {
|
||||||
|
console.error("An error occurred in listenRoom stream:", e);
|
||||||
|
socketManager.removeRoomListener(call, roomMessage.getRoomid()).catch((e) => console.error(e));
|
||||||
call.end();
|
call.end();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -165,9 +211,12 @@ const roomManager: IRoomManagerServer = {
|
|||||||
if (room === null) {
|
if (room === null) {
|
||||||
if (message.hasSubscribetoroom()) {
|
if (message.hasSubscribetoroom()) {
|
||||||
const roomId = message.getSubscribetoroom();
|
const roomId = message.getSubscribetoroom();
|
||||||
socketManager.handleJoinAdminRoom(admin, roomId).then((gameRoom: GameRoom) => {
|
socketManager
|
||||||
|
.handleJoinAdminRoom(admin, roomId)
|
||||||
|
.then((gameRoom: GameRoom) => {
|
||||||
room = gameRoom;
|
room = gameRoom;
|
||||||
});
|
})
|
||||||
|
.catch((e) => console.error(e));
|
||||||
} else {
|
} else {
|
||||||
throw new Error("The first message sent MUST be of type JoinRoomMessage");
|
throw new Error("The first message sent MUST be of type JoinRoomMessage");
|
||||||
}
|
}
|
||||||
@ -192,11 +241,9 @@ const roomManager: IRoomManagerServer = {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
sendAdminMessage(call: ServerUnaryCall<AdminMessage>, callback: sendUnaryData<EmptyMessage>): void {
|
sendAdminMessage(call: ServerUnaryCall<AdminMessage>, callback: sendUnaryData<EmptyMessage>): void {
|
||||||
socketManager.sendAdminMessage(
|
socketManager
|
||||||
call.request.getRoomid(),
|
.sendAdminMessage(call.request.getRoomid(), call.request.getRecipientuuid(), call.request.getMessage())
|
||||||
call.request.getRecipientuuid(),
|
.catch((e) => console.error(e));
|
||||||
call.request.getMessage()
|
|
||||||
);
|
|
||||||
|
|
||||||
callback(null, new EmptyMessage());
|
callback(null, new EmptyMessage());
|
||||||
},
|
},
|
||||||
@ -207,26 +254,33 @@ const roomManager: IRoomManagerServer = {
|
|||||||
},
|
},
|
||||||
ban(call: ServerUnaryCall<BanMessage>, callback: sendUnaryData<EmptyMessage>): void {
|
ban(call: ServerUnaryCall<BanMessage>, callback: sendUnaryData<EmptyMessage>): void {
|
||||||
// FIXME Work in progress
|
// FIXME Work in progress
|
||||||
socketManager.banUser(call.request.getRoomid(), call.request.getRecipientuuid(), call.request.getMessage());
|
socketManager
|
||||||
|
.banUser(call.request.getRoomid(), call.request.getRecipientuuid(), call.request.getMessage())
|
||||||
|
.catch((e) => console.error(e));
|
||||||
|
|
||||||
callback(null, new EmptyMessage());
|
callback(null, new EmptyMessage());
|
||||||
},
|
},
|
||||||
sendAdminMessageToRoom(call: ServerUnaryCall<AdminRoomMessage>, callback: sendUnaryData<EmptyMessage>): void {
|
sendAdminMessageToRoom(call: ServerUnaryCall<AdminRoomMessage>, callback: sendUnaryData<EmptyMessage>): void {
|
||||||
socketManager.sendAdminRoomMessage(call.request.getRoomid(), call.request.getMessage());
|
// FIXME: we could improve return message by returning a Success|ErrorMessage message
|
||||||
|
socketManager
|
||||||
|
.sendAdminRoomMessage(call.request.getRoomid(), call.request.getMessage())
|
||||||
|
.catch((e) => console.error(e));
|
||||||
callback(null, new EmptyMessage());
|
callback(null, new EmptyMessage());
|
||||||
},
|
},
|
||||||
sendWorldFullWarningToRoom(
|
sendWorldFullWarningToRoom(
|
||||||
call: ServerUnaryCall<WorldFullWarningToRoomMessage>,
|
call: ServerUnaryCall<WorldFullWarningToRoomMessage>,
|
||||||
callback: sendUnaryData<EmptyMessage>
|
callback: sendUnaryData<EmptyMessage>
|
||||||
): void {
|
): void {
|
||||||
socketManager.dispatchWorlFullWarning(call.request.getRoomid());
|
// FIXME: we could improve return message by returning a Success|ErrorMessage message
|
||||||
|
socketManager.dispatchWorldFullWarning(call.request.getRoomid()).catch((e) => console.error(e));
|
||||||
callback(null, new EmptyMessage());
|
callback(null, new EmptyMessage());
|
||||||
},
|
},
|
||||||
sendRefreshRoomPrompt(
|
sendRefreshRoomPrompt(
|
||||||
call: ServerUnaryCall<RefreshRoomPromptMessage>,
|
call: ServerUnaryCall<RefreshRoomPromptMessage>,
|
||||||
callback: sendUnaryData<EmptyMessage>
|
callback: sendUnaryData<EmptyMessage>
|
||||||
): void {
|
): void {
|
||||||
socketManager.dispatchRoomRefresh(call.request.getRoomid());
|
// FIXME: we could improve return message by returning a Success|ErrorMessage message
|
||||||
|
socketManager.dispatchRoomRefresh(call.request.getRoomid()).catch((e) => console.error(e));
|
||||||
callback(null, new EmptyMessage());
|
callback(null, new EmptyMessage());
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
24
back/src/Services/AdminApi.ts
Normal file
24
back/src/Services/AdminApi.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { ADMIN_API_TOKEN, ADMIN_API_URL } from "../Enum/EnvironmentVariable";
|
||||||
|
import Axios from "axios";
|
||||||
|
import { MapDetailsData } from "./AdminApi/MapDetailsData";
|
||||||
|
import { RoomRedirect } from "./AdminApi/RoomRedirect";
|
||||||
|
|
||||||
|
class AdminApi {
|
||||||
|
async fetchMapDetails(playUri: string): Promise<MapDetailsData | RoomRedirect> {
|
||||||
|
if (!ADMIN_API_URL) {
|
||||||
|
return Promise.reject(new Error("No admin backoffice set!"));
|
||||||
|
}
|
||||||
|
|
||||||
|
const params: { playUri: string } = {
|
||||||
|
playUri,
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await Axios.get(ADMIN_API_URL + "/api/map", {
|
||||||
|
headers: { Authorization: `${ADMIN_API_TOKEN}` },
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const adminApi = new AdminApi();
|
11
back/src/Services/AdminApi/CharacterTexture.ts
Normal file
11
back/src/Services/AdminApi/CharacterTexture.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import * as tg from "generic-type-guard";
|
||||||
|
|
||||||
|
export const isCharacterTexture = new tg.IsInterface()
|
||||||
|
.withProperties({
|
||||||
|
id: tg.isNumber,
|
||||||
|
level: tg.isNumber,
|
||||||
|
url: tg.isString,
|
||||||
|
rights: tg.isString,
|
||||||
|
})
|
||||||
|
.get();
|
||||||
|
export type CharacterTexture = tg.GuardedType<typeof isCharacterTexture>;
|
21
back/src/Services/AdminApi/MapDetailsData.ts
Normal file
21
back/src/Services/AdminApi/MapDetailsData.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import * as tg from "generic-type-guard";
|
||||||
|
import { isCharacterTexture } from "./CharacterTexture";
|
||||||
|
import { isAny, isNumber } from "generic-type-guard";
|
||||||
|
|
||||||
|
/*const isNumericEnum =
|
||||||
|
<T extends { [n: number]: string }>(vs: T) =>
|
||||||
|
(v: any): v is T =>
|
||||||
|
typeof v === "number" && v in vs;*/
|
||||||
|
|
||||||
|
export const isMapDetailsData = new tg.IsInterface()
|
||||||
|
.withProperties({
|
||||||
|
mapUrl: tg.isString,
|
||||||
|
policy_type: isNumber, //isNumericEnum(GameRoomPolicyTypes),
|
||||||
|
tags: tg.isArray(tg.isString),
|
||||||
|
textures: tg.isArray(isCharacterTexture),
|
||||||
|
})
|
||||||
|
.withOptionalProperties({
|
||||||
|
roomSlug: tg.isUnion(tg.isString, tg.isNull), // deprecated
|
||||||
|
})
|
||||||
|
.get();
|
||||||
|
export type MapDetailsData = tg.GuardedType<typeof isMapDetailsData>;
|
8
back/src/Services/AdminApi/RoomRedirect.ts
Normal file
8
back/src/Services/AdminApi/RoomRedirect.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import * as tg from "generic-type-guard";
|
||||||
|
|
||||||
|
export const isRoomRedirect = new tg.IsInterface()
|
||||||
|
.withProperties({
|
||||||
|
redirectUrl: tg.isString,
|
||||||
|
})
|
||||||
|
.get();
|
||||||
|
export type RoomRedirect = tg.GuardedType<typeof isRoomRedirect>;
|
1
back/src/Services/LocalUrlError.ts
Normal file
1
back/src/Services/LocalUrlError.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export class LocalUrlError extends Error {}
|
67
back/src/Services/MapFetcher.ts
Normal file
67
back/src/Services/MapFetcher.ts
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import Axios from "axios";
|
||||||
|
import ipaddr from "ipaddr.js";
|
||||||
|
import { Resolver } from "dns";
|
||||||
|
import { promisify } from "util";
|
||||||
|
import { LocalUrlError } from "./LocalUrlError";
|
||||||
|
import { ITiledMap } from "@workadventure/tiled-map-type-guard";
|
||||||
|
import { isTiledMap } from "@workadventure/tiled-map-type-guard/dist";
|
||||||
|
|
||||||
|
class MapFetcher {
|
||||||
|
async fetchMap(mapUrl: string): Promise<ITiledMap> {
|
||||||
|
// Before trying to make the query, let's verify the map is actually on the open internet (and not a local test map)
|
||||||
|
|
||||||
|
if (await this.isLocalUrl(mapUrl)) {
|
||||||
|
throw new LocalUrlError('URL for map "' + mapUrl + '" targets a local map');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: mapUrl is provided by the client. A possible attack vector would be to use a rogue DNS server that
|
||||||
|
// returns local URLs. Alas, Axios cannot pin a URL to a given IP. So "isLocalUrl" and Axios.get could potentially
|
||||||
|
// target to different servers (and one could trick Axios.get into loading resources on the internal network
|
||||||
|
// despite isLocalUrl checking that.
|
||||||
|
// We can deem this problem not that important because:
|
||||||
|
// - We make sure we are only passing "GET" requests
|
||||||
|
// - The result of the query is never displayed to the end user
|
||||||
|
const res = await Axios.get(mapUrl, {
|
||||||
|
maxContentLength: 50 * 1024 * 1024, // Max content length: 50MB. Maps should not be bigger
|
||||||
|
timeout: 10000, // Timeout after 10 seconds
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!isTiledMap(res.data)) {
|
||||||
|
throw new Error("Invalid map format for map " + mapUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the domain name is localhost of *.localhost
|
||||||
|
* Returns true if the domain name resolves to an IP address that is "private" (like 10.x.x.x or 192.168.x.x)
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
async isLocalUrl(url: string): Promise<boolean> {
|
||||||
|
const urlObj = new URL(url);
|
||||||
|
if (urlObj.hostname === "localhost" || urlObj.hostname.endsWith(".localhost")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let addresses = [];
|
||||||
|
if (!ipaddr.isValid(urlObj.hostname)) {
|
||||||
|
const resolver = new Resolver();
|
||||||
|
addresses = await promisify(resolver.resolve).bind(resolver)(urlObj.hostname);
|
||||||
|
} else {
|
||||||
|
addresses = [urlObj.hostname];
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const address of addresses) {
|
||||||
|
const addr = ipaddr.parse(address);
|
||||||
|
if (addr.range() !== "unicast") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const mapFetcher = new MapFetcher();
|
@ -1,5 +1,14 @@
|
|||||||
import { ErrorMessage, ServerToClientMessage } from "../Messages/generated/messages_pb";
|
import {
|
||||||
|
BatchMessage,
|
||||||
|
BatchToPusherMessage,
|
||||||
|
BatchToPusherRoomMessage,
|
||||||
|
ErrorMessage,
|
||||||
|
ServerToClientMessage,
|
||||||
|
SubToPusherMessage,
|
||||||
|
SubToPusherRoomMessage,
|
||||||
|
} from "../Messages/generated/messages_pb";
|
||||||
import { UserSocket } from "_Model/User";
|
import { UserSocket } from "_Model/User";
|
||||||
|
import { RoomSocket, ZoneSocket } from "../RoomManager";
|
||||||
|
|
||||||
export function emitError(Client: UserSocket, message: string): void {
|
export function emitError(Client: UserSocket, message: string): void {
|
||||||
const errorMessage = new ErrorMessage();
|
const errorMessage = new ErrorMessage();
|
||||||
@ -13,3 +22,39 @@ export function emitError(Client: UserSocket, message: string): void {
|
|||||||
//}
|
//}
|
||||||
console.warn(message);
|
console.warn(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function emitErrorOnRoomSocket(Client: RoomSocket, message: string): void {
|
||||||
|
console.error(message);
|
||||||
|
|
||||||
|
const errorMessage = new ErrorMessage();
|
||||||
|
errorMessage.setMessage(message);
|
||||||
|
|
||||||
|
const subToPusherRoomMessage = new SubToPusherRoomMessage();
|
||||||
|
subToPusherRoomMessage.setErrormessage(errorMessage);
|
||||||
|
|
||||||
|
const batchToPusherMessage = new BatchToPusherRoomMessage();
|
||||||
|
batchToPusherMessage.addPayload(subToPusherRoomMessage);
|
||||||
|
|
||||||
|
//if (!Client.disconnecting) {
|
||||||
|
Client.write(batchToPusherMessage);
|
||||||
|
//}
|
||||||
|
console.warn(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function emitErrorOnZoneSocket(Client: ZoneSocket, message: string): void {
|
||||||
|
console.error(message);
|
||||||
|
|
||||||
|
const errorMessage = new ErrorMessage();
|
||||||
|
errorMessage.setMessage(message);
|
||||||
|
|
||||||
|
const subToPusherMessage = new SubToPusherMessage();
|
||||||
|
subToPusherMessage.setErrormessage(errorMessage);
|
||||||
|
|
||||||
|
const batchToPusherMessage = new BatchToPusherMessage();
|
||||||
|
batchToPusherMessage.addPayload(subToPusherMessage);
|
||||||
|
|
||||||
|
//if (!Client.disconnecting) {
|
||||||
|
Client.write(batchToPusherMessage);
|
||||||
|
//}
|
||||||
|
console.warn(message);
|
||||||
|
}
|
||||||
|
23
back/src/Services/RedisClient.ts
Normal file
23
back/src/Services/RedisClient.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { ClientOpts, createClient, RedisClient } from "redis";
|
||||||
|
import { REDIS_HOST, REDIS_PASSWORD, REDIS_PORT } from "../Enum/EnvironmentVariable";
|
||||||
|
|
||||||
|
let redisClient: RedisClient | null = null;
|
||||||
|
|
||||||
|
if (REDIS_HOST !== undefined) {
|
||||||
|
const config: ClientOpts = {
|
||||||
|
host: REDIS_HOST,
|
||||||
|
port: REDIS_PORT,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (REDIS_PASSWORD) {
|
||||||
|
config.password = REDIS_PASSWORD;
|
||||||
|
}
|
||||||
|
|
||||||
|
redisClient = createClient(config);
|
||||||
|
|
||||||
|
redisClient.on("error", (err) => {
|
||||||
|
console.error("Error connecting to Redis:", err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export { redisClient };
|
43
back/src/Services/Repository/RedisVariablesRepository.ts
Normal file
43
back/src/Services/Repository/RedisVariablesRepository.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import { promisify } from "util";
|
||||||
|
import { RedisClient } from "redis";
|
||||||
|
import { VariablesRepositoryInterface } from "./VariablesRepositoryInterface";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class in charge of saving/loading variables from the data store
|
||||||
|
*/
|
||||||
|
export class RedisVariablesRepository implements VariablesRepositoryInterface {
|
||||||
|
private readonly hgetall: OmitThisParameter<(arg1: string) => Promise<{ [p: string]: string }>>;
|
||||||
|
private readonly hset: OmitThisParameter<(arg1: [string, ...string[]]) => Promise<number>>;
|
||||||
|
private readonly hdel: OmitThisParameter<(arg1: string, arg2: string) => Promise<number>>;
|
||||||
|
|
||||||
|
constructor(private redisClient: RedisClient) {
|
||||||
|
/* eslint-disable @typescript-eslint/unbound-method */
|
||||||
|
this.hgetall = promisify(redisClient.hgetall).bind(redisClient);
|
||||||
|
this.hset = promisify(redisClient.hset).bind(redisClient);
|
||||||
|
this.hdel = promisify(redisClient.hdel).bind(redisClient);
|
||||||
|
/* eslint-enable @typescript-eslint/unbound-method */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load all variables for a room.
|
||||||
|
*
|
||||||
|
* Note: in Redis, variables are stored in a hashmap and the key is the roomUrl
|
||||||
|
*/
|
||||||
|
async loadVariables(roomUrl: string): Promise<{ [key: string]: string }> {
|
||||||
|
return this.hgetall(roomUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveVariable(roomUrl: string, key: string, value: string): Promise<number> {
|
||||||
|
// The value is passed to JSON.stringify client side. If value is "undefined", JSON.stringify returns "undefined"
|
||||||
|
// which is translated to empty string when fetching the value in the pusher.
|
||||||
|
// Therefore, empty string server side == undefined client side.
|
||||||
|
if (value === "") {
|
||||||
|
return this.hdel(roomUrl, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: SLOW WRITING EVERY 2 SECONDS WITH A TIMEOUT
|
||||||
|
|
||||||
|
// @ts-ignore See https://stackoverflow.com/questions/63539317/how-do-i-use-hmset-with-node-promisify
|
||||||
|
return this.hset(roomUrl, key, value);
|
||||||
|
}
|
||||||
|
}
|
14
back/src/Services/Repository/VariablesRepository.ts
Normal file
14
back/src/Services/Repository/VariablesRepository.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { RedisVariablesRepository } from "./RedisVariablesRepository";
|
||||||
|
import { redisClient } from "../RedisClient";
|
||||||
|
import { VoidVariablesRepository } from "./VoidVariablesRepository";
|
||||||
|
import { VariablesRepositoryInterface } from "./VariablesRepositoryInterface";
|
||||||
|
|
||||||
|
let variablesRepository: VariablesRepositoryInterface;
|
||||||
|
if (!redisClient) {
|
||||||
|
console.warn("WARNING: Redis isnot configured. No variables will be persisted.");
|
||||||
|
variablesRepository = new VoidVariablesRepository();
|
||||||
|
} else {
|
||||||
|
variablesRepository = new RedisVariablesRepository(redisClient);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { variablesRepository };
|
10
back/src/Services/Repository/VariablesRepositoryInterface.ts
Normal file
10
back/src/Services/Repository/VariablesRepositoryInterface.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
export interface VariablesRepositoryInterface {
|
||||||
|
/**
|
||||||
|
* Load all variables for a room.
|
||||||
|
*
|
||||||
|
* Note: in Redis, variables are stored in a hashmap and the key is the roomUrl
|
||||||
|
*/
|
||||||
|
loadVariables(roomUrl: string): Promise<{ [key: string]: string }>;
|
||||||
|
|
||||||
|
saveVariable(roomUrl: string, key: string, value: string): Promise<number>;
|
||||||
|
}
|
14
back/src/Services/Repository/VoidVariablesRepository.ts
Normal file
14
back/src/Services/Repository/VoidVariablesRepository.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { VariablesRepositoryInterface } from "./VariablesRepositoryInterface";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mock class in charge of NOT saving/loading variables from the data store
|
||||||
|
*/
|
||||||
|
export class VoidVariablesRepository implements VariablesRepositoryInterface {
|
||||||
|
loadVariables(roomUrl: string): Promise<{ [key: string]: string }> {
|
||||||
|
return Promise.resolve({});
|
||||||
|
}
|
||||||
|
|
||||||
|
saveVariable(roomUrl: string, key: string, value: string): Promise<number> {
|
||||||
|
return Promise.resolve(0);
|
||||||
|
}
|
||||||
|
}
|
@ -30,6 +30,9 @@ import {
|
|||||||
BanUserMessage,
|
BanUserMessage,
|
||||||
RefreshRoomMessage,
|
RefreshRoomMessage,
|
||||||
EmotePromptMessage,
|
EmotePromptMessage,
|
||||||
|
VariableMessage,
|
||||||
|
BatchToPusherRoomMessage,
|
||||||
|
SubToPusherRoomMessage,
|
||||||
} from "../Messages/generated/messages_pb";
|
} from "../Messages/generated/messages_pb";
|
||||||
import { User, UserSocket } from "../Model/User";
|
import { User, UserSocket } from "../Model/User";
|
||||||
import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils";
|
import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils";
|
||||||
@ -48,7 +51,7 @@ import Jwt from "jsonwebtoken";
|
|||||||
import { JITSI_URL } from "../Enum/EnvironmentVariable";
|
import { JITSI_URL } from "../Enum/EnvironmentVariable";
|
||||||
import { clientEventsEmitter } from "./ClientEventsEmitter";
|
import { clientEventsEmitter } from "./ClientEventsEmitter";
|
||||||
import { gaugeManager } from "./GaugeManager";
|
import { gaugeManager } from "./GaugeManager";
|
||||||
import { ZoneSocket } from "../RoomManager";
|
import { RoomSocket, ZoneSocket } from "../RoomManager";
|
||||||
import { Zone } from "_Model/Zone";
|
import { Zone } from "_Model/Zone";
|
||||||
import Debug from "debug";
|
import Debug from "debug";
|
||||||
import { Admin } from "_Model/Admin";
|
import { Admin } from "_Model/Admin";
|
||||||
@ -65,7 +68,9 @@ function emitZoneMessage(subMessage: SubToPusherMessage, socket: ZoneSocket): vo
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class SocketManager {
|
export class SocketManager {
|
||||||
private rooms: Map<string, GameRoom> = new Map<string, GameRoom>();
|
//private rooms = new Map<string, GameRoom>();
|
||||||
|
// List of rooms in process of loading.
|
||||||
|
private roomsPromises = new Map<string, PromiseLike<GameRoom>>();
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
clientEventsEmitter.registerToClientJoin((clientUUid: string, roomId: string) => {
|
clientEventsEmitter.registerToClientJoin((clientUUid: string, roomId: string) => {
|
||||||
@ -101,6 +106,16 @@ export class SocketManager {
|
|||||||
roomJoinedMessage.addItem(itemStateMessage);
|
roomJoinedMessage.addItem(itemStateMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const variables = await room.getVariablesForTags(user.tags);
|
||||||
|
|
||||||
|
for (const [name, value] of variables.entries()) {
|
||||||
|
const variableMessage = new VariableMessage();
|
||||||
|
variableMessage.setName(name);
|
||||||
|
variableMessage.setValue(value);
|
||||||
|
|
||||||
|
roomJoinedMessage.addVariable(variableMessage);
|
||||||
|
}
|
||||||
|
|
||||||
roomJoinedMessage.setCurrentuserid(user.id);
|
roomJoinedMessage.setCurrentuserid(user.id);
|
||||||
|
|
||||||
const serverToClientMessage = new ServerToClientMessage();
|
const serverToClientMessage = new ServerToClientMessage();
|
||||||
@ -114,7 +129,6 @@ export class SocketManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleUserMovesMessage(room: GameRoom, user: User, userMovesMessage: UserMovesMessage) {
|
handleUserMovesMessage(room: GameRoom, user: User, userMovesMessage: UserMovesMessage) {
|
||||||
try {
|
|
||||||
const userMoves = userMovesMessage.toObject();
|
const userMoves = userMovesMessage.toObject();
|
||||||
const position = userMovesMessage.getPosition();
|
const position = userMovesMessage.getPosition();
|
||||||
|
|
||||||
@ -134,10 +148,6 @@ export class SocketManager {
|
|||||||
// update position in the world
|
// update position in the world
|
||||||
room.updatePosition(user, ProtobufUtils.toPointInterface(position));
|
room.updatePosition(user, ProtobufUtils.toPointInterface(position));
|
||||||
//room.setViewport(client, client.viewport);
|
//room.setViewport(client, client.viewport);
|
||||||
} catch (e) {
|
|
||||||
console.error('An error occurred on "user_position" event');
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Useless now, will be useful again if we allow editing details in game
|
// Useless now, will be useful again if we allow editing details in game
|
||||||
@ -156,18 +166,12 @@ export class SocketManager {
|
|||||||
}*/
|
}*/
|
||||||
|
|
||||||
handleSilentMessage(room: GameRoom, user: User, silentMessage: SilentMessage) {
|
handleSilentMessage(room: GameRoom, user: User, silentMessage: SilentMessage) {
|
||||||
try {
|
|
||||||
room.setSilent(user, silentMessage.getSilent());
|
room.setSilent(user, silentMessage.getSilent());
|
||||||
} catch (e) {
|
|
||||||
console.error('An error occurred on "handleSilentMessage"');
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleItemEvent(room: GameRoom, user: User, itemEventMessage: ItemEventMessage) {
|
handleItemEvent(room: GameRoom, user: User, itemEventMessage: ItemEventMessage) {
|
||||||
const itemEvent = ProtobufUtils.toItemEvent(itemEventMessage);
|
const itemEvent = ProtobufUtils.toItemEvent(itemEventMessage);
|
||||||
|
|
||||||
try {
|
|
||||||
const subMessage = new SubMessage();
|
const subMessage = new SubMessage();
|
||||||
subMessage.setItemeventmessage(itemEventMessage);
|
subMessage.setItemeventmessage(itemEventMessage);
|
||||||
|
|
||||||
@ -178,10 +182,10 @@ export class SocketManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
room.setItemState(itemEvent.itemId, itemEvent.state);
|
room.setItemState(itemEvent.itemId, itemEvent.state);
|
||||||
} catch (e) {
|
|
||||||
console.error('An error occurred on "item_event"');
|
|
||||||
console.error(e);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleVariableEvent(room: GameRoom, user: User, variableMessage: VariableMessage): Promise<void> {
|
||||||
|
return room.setVariable(variableMessage.getName(), variableMessage.getValue(), user);
|
||||||
}
|
}
|
||||||
|
|
||||||
emitVideo(room: GameRoom, user: User, data: WebRtcSignalToServerMessage): void {
|
emitVideo(room: GameRoom, user: User, data: WebRtcSignalToServerMessage): void {
|
||||||
@ -250,7 +254,7 @@ export class SocketManager {
|
|||||||
//user leave previous world
|
//user leave previous world
|
||||||
room.leave(user);
|
room.leave(user);
|
||||||
if (room.isEmpty()) {
|
if (room.isEmpty()) {
|
||||||
this.rooms.delete(room.roomUrl);
|
this.roomsPromises.delete(room.roomUrl);
|
||||||
gaugeManager.decNbRoomGauge();
|
gaugeManager.decNbRoomGauge();
|
||||||
debug('Room is empty. Deleting room "%s"', room.roomUrl);
|
debug('Room is empty. Deleting room "%s"', room.roomUrl);
|
||||||
}
|
}
|
||||||
@ -261,10 +265,10 @@ export class SocketManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getOrCreateRoom(roomId: string): Promise<GameRoom> {
|
async getOrCreateRoom(roomId: string): Promise<GameRoom> {
|
||||||
//check and create new world for a room
|
//check and create new room
|
||||||
let world = this.rooms.get(roomId);
|
let roomPromise = this.roomsPromises.get(roomId);
|
||||||
if (world === undefined) {
|
if (roomPromise === undefined) {
|
||||||
world = new GameRoom(
|
roomPromise = GameRoom.create(
|
||||||
roomId,
|
roomId,
|
||||||
(user: User, group: Group) => this.joinWebRtcRoom(user, group),
|
(user: User, group: Group) => this.joinWebRtcRoom(user, group),
|
||||||
(user: User, group: Group) => this.disConnectedUser(user, group),
|
(user: User, group: Group) => this.disConnectedUser(user, group),
|
||||||
@ -278,11 +282,18 @@ export class SocketManager {
|
|||||||
this.onClientLeave(thing, newZone, listener),
|
this.onClientLeave(thing, newZone, listener),
|
||||||
(emoteEventMessage: EmoteEventMessage, listener: ZoneSocket) =>
|
(emoteEventMessage: EmoteEventMessage, listener: ZoneSocket) =>
|
||||||
this.onEmote(emoteEventMessage, listener)
|
this.onEmote(emoteEventMessage, listener)
|
||||||
);
|
)
|
||||||
|
.then((gameRoom) => {
|
||||||
gaugeManager.incNbRoomGauge();
|
gaugeManager.incNbRoomGauge();
|
||||||
this.rooms.set(roomId, world);
|
return gameRoom;
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
this.roomsPromises.delete(roomId);
|
||||||
|
throw e;
|
||||||
|
});
|
||||||
|
this.roomsPromises.set(roomId, roomPromise);
|
||||||
}
|
}
|
||||||
return Promise.resolve(world);
|
return roomPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async joinRoom(
|
private async joinRoom(
|
||||||
@ -508,21 +519,16 @@ export class SocketManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
emitPlayGlobalMessage(room: GameRoom, playGlobalMessage: PlayGlobalMessage) {
|
emitPlayGlobalMessage(room: GameRoom, playGlobalMessage: PlayGlobalMessage) {
|
||||||
try {
|
|
||||||
const serverToClientMessage = new ServerToClientMessage();
|
const serverToClientMessage = new ServerToClientMessage();
|
||||||
serverToClientMessage.setPlayglobalmessage(playGlobalMessage);
|
serverToClientMessage.setPlayglobalmessage(playGlobalMessage);
|
||||||
|
|
||||||
for (const [id, user] of room.getUsers().entries()) {
|
for (const [id, user] of room.getUsers().entries()) {
|
||||||
user.socket.write(serverToClientMessage);
|
user.socket.write(serverToClientMessage);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
|
||||||
console.error('An error occurred on "emitPlayGlobalMessage" event');
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public getWorlds(): Map<string, GameRoom> {
|
public getWorlds(): Map<string, PromiseLike<GameRoom>> {
|
||||||
return this.rooms;
|
return this.roomsPromises;
|
||||||
}
|
}
|
||||||
|
|
||||||
public handleQueryJitsiJwtMessage(user: User, queryJitsiJwtMessage: QueryJitsiJwtMessage) {
|
public handleQueryJitsiJwtMessage(user: User, queryJitsiJwtMessage: QueryJitsiJwtMessage) {
|
||||||
@ -592,11 +598,10 @@ export class SocketManager {
|
|||||||
}, 10000);
|
}, 10000);
|
||||||
}
|
}
|
||||||
|
|
||||||
public addZoneListener(call: ZoneSocket, roomId: string, x: number, y: number): void {
|
public async addZoneListener(call: ZoneSocket, roomId: string, x: number, y: number): Promise<void> {
|
||||||
const room = this.rooms.get(roomId);
|
const room = await this.roomsPromises.get(roomId);
|
||||||
if (!room) {
|
if (!room) {
|
||||||
console.error("In addZoneListener, could not find room with id '" + roomId + "'");
|
throw new Error("In addZoneListener, could not find room with id '" + roomId + "'");
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const things = room.addZoneListener(call, x, y);
|
const things = room.addZoneListener(call, x, y);
|
||||||
@ -637,16 +642,37 @@ export class SocketManager {
|
|||||||
call.write(batchMessage);
|
call.write(batchMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
removeZoneListener(call: ZoneSocket, roomId: string, x: number, y: number) {
|
async removeZoneListener(call: ZoneSocket, roomId: string, x: number, y: number): Promise<void> {
|
||||||
const room = this.rooms.get(roomId);
|
const room = await this.roomsPromises.get(roomId);
|
||||||
if (!room) {
|
if (!room) {
|
||||||
console.error("In removeZoneListener, could not find room with id '" + roomId + "'");
|
throw new Error("In removeZoneListener, could not find room with id '" + roomId + "'");
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
room.removeZoneListener(call, x, y);
|
room.removeZoneListener(call, x, y);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async addRoomListener(call: RoomSocket, roomId: string) {
|
||||||
|
const room = await this.getOrCreateRoom(roomId);
|
||||||
|
if (!room) {
|
||||||
|
throw new Error("In addRoomListener, could not find room with id '" + roomId + "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
room.addRoomListener(call);
|
||||||
|
|
||||||
|
const batchMessage = new BatchToPusherRoomMessage();
|
||||||
|
|
||||||
|
call.write(batchMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeRoomListener(call: RoomSocket, roomId: string) {
|
||||||
|
const room = await this.roomsPromises.get(roomId);
|
||||||
|
if (!room) {
|
||||||
|
throw new Error("In removeRoomListener, could not find room with id '" + roomId + "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
room.removeRoomListener(call);
|
||||||
|
}
|
||||||
|
|
||||||
public async handleJoinAdminRoom(admin: Admin, roomId: string): Promise<GameRoom> {
|
public async handleJoinAdminRoom(admin: Admin, roomId: string): Promise<GameRoom> {
|
||||||
const room = await socketManager.getOrCreateRoom(roomId);
|
const room = await socketManager.getOrCreateRoom(roomId);
|
||||||
|
|
||||||
@ -658,14 +684,14 @@ export class SocketManager {
|
|||||||
public leaveAdminRoom(room: GameRoom, admin: Admin) {
|
public leaveAdminRoom(room: GameRoom, admin: Admin) {
|
||||||
room.adminLeave(admin);
|
room.adminLeave(admin);
|
||||||
if (room.isEmpty()) {
|
if (room.isEmpty()) {
|
||||||
this.rooms.delete(room.roomUrl);
|
this.roomsPromises.delete(room.roomUrl);
|
||||||
gaugeManager.decNbRoomGauge();
|
gaugeManager.decNbRoomGauge();
|
||||||
debug('Room is empty. Deleting room "%s"', room.roomUrl);
|
debug('Room is empty. Deleting room "%s"', room.roomUrl);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public sendAdminMessage(roomId: string, recipientUuid: string, message: string): void {
|
public async sendAdminMessage(roomId: string, recipientUuid: string, message: string): Promise<void> {
|
||||||
const room = this.rooms.get(roomId);
|
const room = await this.roomsPromises.get(roomId);
|
||||||
if (!room) {
|
if (!room) {
|
||||||
console.error(
|
console.error(
|
||||||
"In sendAdminMessage, could not find room with id '" +
|
"In sendAdminMessage, could not find room with id '" +
|
||||||
@ -695,8 +721,8 @@ export class SocketManager {
|
|||||||
recipient.socket.write(serverToClientMessage);
|
recipient.socket.write(serverToClientMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
public banUser(roomId: string, recipientUuid: string, message: string): void {
|
public async banUser(roomId: string, recipientUuid: string, message: string): Promise<void> {
|
||||||
const room = this.rooms.get(roomId);
|
const room = await this.roomsPromises.get(roomId);
|
||||||
if (!room) {
|
if (!room) {
|
||||||
console.error(
|
console.error(
|
||||||
"In banUser, could not find room with id '" +
|
"In banUser, could not find room with id '" +
|
||||||
@ -731,8 +757,8 @@ export class SocketManager {
|
|||||||
recipient.socket.end();
|
recipient.socket.end();
|
||||||
}
|
}
|
||||||
|
|
||||||
sendAdminRoomMessage(roomId: string, message: string) {
|
async sendAdminRoomMessage(roomId: string, message: string) {
|
||||||
const room = this.rooms.get(roomId);
|
const room = await this.roomsPromises.get(roomId);
|
||||||
if (!room) {
|
if (!room) {
|
||||||
//todo: this should cause the http call to return a 500
|
//todo: this should cause the http call to return a 500
|
||||||
console.error(
|
console.error(
|
||||||
@ -755,8 +781,8 @@ export class SocketManager {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatchWorlFullWarning(roomId: string): void {
|
async dispatchWorldFullWarning(roomId: string): Promise<void> {
|
||||||
const room = this.rooms.get(roomId);
|
const room = await this.roomsPromises.get(roomId);
|
||||||
if (!room) {
|
if (!room) {
|
||||||
//todo: this should cause the http call to return a 500
|
//todo: this should cause the http call to return a 500
|
||||||
console.error(
|
console.error(
|
||||||
@ -777,8 +803,8 @@ export class SocketManager {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatchRoomRefresh(roomId: string): void {
|
async dispatchRoomRefresh(roomId: string): Promise<void> {
|
||||||
const room = this.rooms.get(roomId);
|
const room = await this.roomsPromises.get(roomId);
|
||||||
if (!room) {
|
if (!room) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
187
back/src/Services/VariablesManager.ts
Normal file
187
back/src/Services/VariablesManager.ts
Normal file
@ -0,0 +1,187 @@
|
|||||||
|
/**
|
||||||
|
* Handles variables shared between the scripting API and the server.
|
||||||
|
*/
|
||||||
|
import { ITiledMap, ITiledMapObject, ITiledMapObjectLayer } from "@workadventure/tiled-map-type-guard/dist";
|
||||||
|
import { User } from "_Model/User";
|
||||||
|
import { variablesRepository } from "./Repository/VariablesRepository";
|
||||||
|
import { redisClient } from "./RedisClient";
|
||||||
|
|
||||||
|
interface Variable {
|
||||||
|
defaultValue?: string;
|
||||||
|
persist?: boolean;
|
||||||
|
readableBy?: string;
|
||||||
|
writableBy?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class VariablesManager {
|
||||||
|
/**
|
||||||
|
* The actual values of the variables for the current room
|
||||||
|
*/
|
||||||
|
private _variables = new Map<string, string>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The list of variables that are allowed
|
||||||
|
*/
|
||||||
|
private variableObjects: Map<string, Variable> | undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param map The map can be "null" if it is hosted on a private network. In this case, we assume this is a test setup and bypass any server-side checks.
|
||||||
|
*/
|
||||||
|
constructor(private roomUrl: string, private map: ITiledMap | null) {
|
||||||
|
// We initialize the list of variable object at room start. The objects cannot be edited later
|
||||||
|
// (otherwise, this would cause a security issue if the scripting API can edit this list of objects)
|
||||||
|
if (map) {
|
||||||
|
this.variableObjects = VariablesManager.findVariablesInMap(map);
|
||||||
|
|
||||||
|
// Let's initialize default values
|
||||||
|
for (const [name, variableObject] of this.variableObjects.entries()) {
|
||||||
|
if (variableObject.defaultValue !== undefined) {
|
||||||
|
this._variables.set(name, variableObject.defaultValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Let's load data from the Redis backend.
|
||||||
|
*/
|
||||||
|
public async init(): Promise<VariablesManager> {
|
||||||
|
if (!this.shouldPersist()) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
const variables = await variablesRepository.loadVariables(this.roomUrl);
|
||||||
|
for (const key in variables) {
|
||||||
|
this._variables.set(key, variables[key]);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if saving should be enabled, and false otherwise.
|
||||||
|
*
|
||||||
|
* Saving is enabled if REDIS_HOST is set
|
||||||
|
* unless we are editing a local map
|
||||||
|
* unless we are in dev mode in which case it is ok to save
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private shouldPersist(): boolean {
|
||||||
|
return redisClient !== null && (this.map !== null || process.env.NODE_ENV === "development");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static findVariablesInMap(map: ITiledMap): Map<string, Variable> {
|
||||||
|
const objects = new Map<string, Variable>();
|
||||||
|
for (const layer of map.layers) {
|
||||||
|
if (layer.type === "objectgroup") {
|
||||||
|
for (const object of (layer as ITiledMapObjectLayer).objects) {
|
||||||
|
if (object.type === "variable") {
|
||||||
|
if (object.template) {
|
||||||
|
console.warn(
|
||||||
|
'Warning, a variable object is using a Tiled "template". WorkAdventure does not support objects generated from Tiled templates.'
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We store a copy of the object (to make it immutable)
|
||||||
|
objects.set(object.name, this.iTiledObjectToVariable(object));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return objects;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static iTiledObjectToVariable(object: ITiledMapObject): Variable {
|
||||||
|
const variable: Variable = {};
|
||||||
|
|
||||||
|
if (object.properties) {
|
||||||
|
for (const property of object.properties) {
|
||||||
|
const value = property.value;
|
||||||
|
switch (property.name) {
|
||||||
|
case "default":
|
||||||
|
variable.defaultValue = JSON.stringify(value);
|
||||||
|
break;
|
||||||
|
case "persist":
|
||||||
|
if (typeof value !== "boolean") {
|
||||||
|
throw new Error('The persist property of variable "' + object.name + '" must be a boolean');
|
||||||
|
}
|
||||||
|
variable.persist = value;
|
||||||
|
break;
|
||||||
|
case "writableBy":
|
||||||
|
if (typeof value !== "string") {
|
||||||
|
throw new Error(
|
||||||
|
'The writableBy property of variable "' + object.name + '" must be a string'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (value) {
|
||||||
|
variable.writableBy = value;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "readableBy":
|
||||||
|
if (typeof value !== "string") {
|
||||||
|
throw new Error(
|
||||||
|
'The readableBy property of variable "' + object.name + '" must be a string'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (value) {
|
||||||
|
variable.readableBy = value;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return variable;
|
||||||
|
}
|
||||||
|
|
||||||
|
setVariable(name: string, value: string, user: User): string | undefined {
|
||||||
|
let readableBy: string | undefined;
|
||||||
|
if (this.variableObjects) {
|
||||||
|
const variableObject = this.variableObjects.get(name);
|
||||||
|
if (variableObject === undefined) {
|
||||||
|
throw new Error('Trying to set a variable "' + name + '" that is not defined as an object in the map.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (variableObject.writableBy && !user.tags.includes(variableObject.writableBy)) {
|
||||||
|
throw new Error(
|
||||||
|
'Trying to set a variable "' +
|
||||||
|
name +
|
||||||
|
'". User "' +
|
||||||
|
user.name +
|
||||||
|
'" does not have sufficient permission. Required tag: "' +
|
||||||
|
variableObject.writableBy +
|
||||||
|
'". User tags: ' +
|
||||||
|
user.tags.join(", ") +
|
||||||
|
"."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
readableBy = variableObject.readableBy;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._variables.set(name, value);
|
||||||
|
variablesRepository
|
||||||
|
.saveVariable(this.roomUrl, name, value)
|
||||||
|
.catch((e) => console.error("Error while saving variable in Redis:", e));
|
||||||
|
return readableBy;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getVariablesForTags(tags: string[]): Map<string, string> {
|
||||||
|
if (this.variableObjects === undefined) {
|
||||||
|
return this._variables;
|
||||||
|
}
|
||||||
|
|
||||||
|
const readableVariables = new Map<string, string>();
|
||||||
|
|
||||||
|
for (const [key, value] of this._variables.entries()) {
|
||||||
|
const variableObject = this.variableObjects.get(key);
|
||||||
|
if (variableObject === undefined) {
|
||||||
|
throw new Error('Unexpected variable "' + key + '" found has no associated variableObject.');
|
||||||
|
}
|
||||||
|
if (!variableObject.readableBy || tags.includes(variableObject.readableBy)) {
|
||||||
|
readableVariables.set(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return readableVariables;
|
||||||
|
}
|
||||||
|
}
|
@ -37,7 +37,7 @@ function createJoinRoomMessage(uuid: string, x: number, y: number): JoinRoomMess
|
|||||||
const emote: EmoteCallback = (emoteEventMessage, listener): void => {}
|
const emote: EmoteCallback = (emoteEventMessage, listener): void => {}
|
||||||
|
|
||||||
describe("GameRoom", () => {
|
describe("GameRoom", () => {
|
||||||
it("should connect user1 and user2", () => {
|
it("should connect user1 and user2", async () => {
|
||||||
let connectCalledNumber: number = 0;
|
let connectCalledNumber: number = 0;
|
||||||
const connect: ConnectCallback = (user: User, group: Group): void => {
|
const connect: ConnectCallback = (user: User, group: Group): void => {
|
||||||
connectCalledNumber++;
|
connectCalledNumber++;
|
||||||
@ -47,8 +47,7 @@ describe("GameRoom", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const world = new GameRoom('_/global/test.json', connect, disconnect, 160, 160, () => {}, () => {}, () => {}, emote);
|
const world = await GameRoom.create('https://play.workadventu.re/_/global/localhost/test.json', connect, disconnect, 160, 160, () => {}, () => {}, () => {}, emote);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const user1 = world.join(createMockUserSocket(), createJoinRoomMessage('1', 100, 100));
|
const user1 = world.join(createMockUserSocket(), createJoinRoomMessage('1', 100, 100));
|
||||||
@ -67,7 +66,7 @@ describe("GameRoom", () => {
|
|||||||
expect(connectCalledNumber).toBe(2);
|
expect(connectCalledNumber).toBe(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should connect 3 users", () => {
|
it("should connect 3 users", async () => {
|
||||||
let connectCalled: boolean = false;
|
let connectCalled: boolean = false;
|
||||||
const connect: ConnectCallback = (user: User, group: Group): void => {
|
const connect: ConnectCallback = (user: User, group: Group): void => {
|
||||||
connectCalled = true;
|
connectCalled = true;
|
||||||
@ -76,7 +75,7 @@ describe("GameRoom", () => {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const world = new GameRoom('_/global/test.json', connect, disconnect, 160, 160, () => {}, () => {}, () => {}, emote);
|
const world = await GameRoom.create('https://play.workadventu.re/_/global/localhost/test.json', connect, disconnect, 160, 160, () => {}, () => {}, () => {}, emote);
|
||||||
|
|
||||||
const user1 = world.join(createMockUserSocket(), createJoinRoomMessage('1', 100, 100));
|
const user1 = world.join(createMockUserSocket(), createJoinRoomMessage('1', 100, 100));
|
||||||
|
|
||||||
@ -95,7 +94,7 @@ describe("GameRoom", () => {
|
|||||||
expect(connectCalled).toBe(true);
|
expect(connectCalled).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should disconnect user1 and user2", () => {
|
it("should disconnect user1 and user2", async () => {
|
||||||
let connectCalled: boolean = false;
|
let connectCalled: boolean = false;
|
||||||
let disconnectCallNumber: number = 0;
|
let disconnectCallNumber: number = 0;
|
||||||
const connect: ConnectCallback = (user: User, group: Group): void => {
|
const connect: ConnectCallback = (user: User, group: Group): void => {
|
||||||
@ -105,7 +104,7 @@ describe("GameRoom", () => {
|
|||||||
disconnectCallNumber++;
|
disconnectCallNumber++;
|
||||||
}
|
}
|
||||||
|
|
||||||
const world = new GameRoom('_/global/test.json', connect, disconnect, 160, 160, () => {}, () => {}, () => {}, emote);
|
const world = await GameRoom.create('https://play.workadventu.re/_/global/localhost/test.json', connect, disconnect, 160, 160, () => {}, () => {}, () => {}, emote);
|
||||||
|
|
||||||
const user1 = world.join(createMockUserSocket(), createJoinRoomMessage('1', 100, 100));
|
const user1 = world.join(createMockUserSocket(), createJoinRoomMessage('1', 100, 100));
|
||||||
|
|
||||||
|
32
back/tests/MapFetcherTest.ts
Normal file
32
back/tests/MapFetcherTest.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { arrayIntersect } from "../src/Services/ArrayHelper";
|
||||||
|
import { mapFetcher } from "../src/Services/MapFetcher";
|
||||||
|
|
||||||
|
describe("MapFetcher", () => {
|
||||||
|
it("should return true on localhost ending URLs", async () => {
|
||||||
|
expect(await mapFetcher.isLocalUrl("https://localhost")).toBeTrue();
|
||||||
|
expect(await mapFetcher.isLocalUrl("https://foo.localhost")).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return true on DNS resolving to a local domain", async () => {
|
||||||
|
expect(await mapFetcher.isLocalUrl("https://127.0.0.1.nip.io")).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return true on an IP resolving to a local domain", async () => {
|
||||||
|
expect(await mapFetcher.isLocalUrl("https://127.0.0.1")).toBeTrue();
|
||||||
|
expect(await mapFetcher.isLocalUrl("https://192.168.0.1")).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false on an IP resolving to a global domain", async () => {
|
||||||
|
expect(await mapFetcher.isLocalUrl("https://51.12.42.42")).toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false on an DNS resolving to a global domain", async () => {
|
||||||
|
expect(await mapFetcher.isLocalUrl("https://maps.workadventu.re")).toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw error on invalid domain", async () => {
|
||||||
|
await expectAsync(
|
||||||
|
mapFetcher.isLocalUrl("https://this.domain.name.doesnotexistfoobgjkgfdjkgldf.com")
|
||||||
|
).toBeRejected();
|
||||||
|
});
|
||||||
|
});
|
@ -3,7 +3,7 @@
|
|||||||
"experimentalDecorators": true,
|
"experimentalDecorators": true,
|
||||||
/* Basic Options */
|
/* Basic Options */
|
||||||
// "incremental": true, /* Enable incremental compilation */
|
// "incremental": true, /* Enable incremental compilation */
|
||||||
"target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
|
"target": "ES2019", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
|
||||||
"downlevelIteration": true,
|
"downlevelIteration": true,
|
||||||
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
|
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
|
||||||
// "lib": [], /* Specify library files to be included in the compilation. */
|
// "lib": [], /* Specify library files to be included in the compilation. */
|
||||||
|
@ -122,6 +122,13 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
|
resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
|
||||||
integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==
|
integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==
|
||||||
|
|
||||||
|
"@types/redis@^2.8.31":
|
||||||
|
version "2.8.31"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/redis/-/redis-2.8.31.tgz#c11c1b269fec132ac2ec9eb891edf72fc549149e"
|
||||||
|
integrity sha512-daWrrTDYaa5iSDFbgzZ9gOOzyp2AJmYK59OlG/2KGBgYWF3lfs8GDKm1c//tik5Uc93hDD36O+qLPvzDolChbA==
|
||||||
|
dependencies:
|
||||||
|
"@types/node" "*"
|
||||||
|
|
||||||
"@types/strip-bom@^3.0.0":
|
"@types/strip-bom@^3.0.0":
|
||||||
version "3.0.0"
|
version "3.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/@types/strip-bom/-/strip-bom-3.0.0.tgz#14a8ec3956c2e81edb7520790aecf21c290aebd2"
|
resolved "https://registry.yarnpkg.com/@types/strip-bom/-/strip-bom-3.0.0.tgz#14a8ec3956c2e81edb7520790aecf21c290aebd2"
|
||||||
@ -187,6 +194,13 @@
|
|||||||
semver "^7.3.2"
|
semver "^7.3.2"
|
||||||
tsutils "^3.17.1"
|
tsutils "^3.17.1"
|
||||||
|
|
||||||
|
"@workadventure/tiled-map-type-guard@^1.0.0":
|
||||||
|
version "1.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@workadventure/tiled-map-type-guard/-/tiled-map-type-guard-1.0.0.tgz#02524602ee8b2688429a1f56df1d04da3fc171ba"
|
||||||
|
integrity sha512-Mc0SE128otQnYlScQWVaQVyu1+CkailU/FTBh09UTrVnBAhyMO+jIn9vT9+Dv244xq+uzgQDpXmiVdjgrYFQ+A==
|
||||||
|
dependencies:
|
||||||
|
generic-type-guard "^3.4.1"
|
||||||
|
|
||||||
abbrev@1:
|
abbrev@1:
|
||||||
version "1.1.1"
|
version "1.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
|
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
|
||||||
@ -797,6 +811,11 @@ delegates@^1.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
|
resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
|
||||||
integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=
|
integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=
|
||||||
|
|
||||||
|
denque@^1.5.0:
|
||||||
|
version "1.5.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/denque/-/denque-1.5.0.tgz#773de0686ff2d8ec2ff92914316a47b73b1c73de"
|
||||||
|
integrity sha512-CYiCSgIF1p6EUByQPlGkKnP1M9g0ZV3qMIrqMqZqdwazygIA/YP2vrbcyl1h/WppKJTdl1F85cXIle+394iDAQ==
|
||||||
|
|
||||||
detect-libc@^1.0.2:
|
detect-libc@^1.0.2:
|
||||||
version "1.0.3"
|
version "1.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b"
|
resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b"
|
||||||
@ -1181,6 +1200,11 @@ generic-type-guard@^3.2.0:
|
|||||||
resolved "https://registry.yarnpkg.com/generic-type-guard/-/generic-type-guard-3.3.3.tgz#954b846fecff91047cadb0dcc28930811fcb9dc1"
|
resolved "https://registry.yarnpkg.com/generic-type-guard/-/generic-type-guard-3.3.3.tgz#954b846fecff91047cadb0dcc28930811fcb9dc1"
|
||||||
integrity sha512-SXraZvNW/uTfHVgB48iEwWaD1XFJ1nvZ8QP6qy9pSgaScEyQqFHYN5E6d6rCsJgrvlWKygPrNum7QeJHegzNuQ==
|
integrity sha512-SXraZvNW/uTfHVgB48iEwWaD1XFJ1nvZ8QP6qy9pSgaScEyQqFHYN5E6d6rCsJgrvlWKygPrNum7QeJHegzNuQ==
|
||||||
|
|
||||||
|
generic-type-guard@^3.4.1:
|
||||||
|
version "3.4.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/generic-type-guard/-/generic-type-guard-3.4.1.tgz#0896dc018de915c890562a34763858076e4676da"
|
||||||
|
integrity sha512-sXce0Lz3Wfy2rR1W8O8kUemgEriTeG1x8shqSJeWGb0FwJu2qBEkB1M2qXbdSLmpgDnHcIXo0Dj/1VLNJkK/QA==
|
||||||
|
|
||||||
get-own-enumerable-property-symbols@^3.0.0:
|
get-own-enumerable-property-symbols@^3.0.0:
|
||||||
version "3.0.2"
|
version "3.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz#b5fde77f22cbe35f390b4e089922c50bce6ef664"
|
resolved "https://registry.yarnpkg.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz#b5fde77f22cbe35f390b4e089922c50bce6ef664"
|
||||||
@ -1417,6 +1441,11 @@ invert-kv@^1.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6"
|
resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6"
|
||||||
integrity sha1-EEqOSqym09jNFXqO+L+rLXo//bY=
|
integrity sha1-EEqOSqym09jNFXqO+L+rLXo//bY=
|
||||||
|
|
||||||
|
ipaddr.js@^2.0.1:
|
||||||
|
version "2.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-2.0.1.tgz#eca256a7a877e917aeb368b0a7497ddf42ef81c0"
|
||||||
|
integrity sha512-1qTgH9NG+IIJ4yfKs2e6Pp1bZg8wbDbKHT21HrLIeYBTRLgMYKnMTPAuI3Lcs61nfx5h1xlXnbJtH1kX5/d/ng==
|
||||||
|
|
||||||
is-accessor-descriptor@^0.1.6:
|
is-accessor-descriptor@^0.1.6:
|
||||||
version "0.1.6"
|
version "0.1.6"
|
||||||
resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6"
|
resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6"
|
||||||
@ -2424,6 +2453,33 @@ redent@^1.0.0:
|
|||||||
indent-string "^2.1.0"
|
indent-string "^2.1.0"
|
||||||
strip-indent "^1.0.1"
|
strip-indent "^1.0.1"
|
||||||
|
|
||||||
|
redis-commands@^1.7.0:
|
||||||
|
version "1.7.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/redis-commands/-/redis-commands-1.7.0.tgz#15a6fea2d58281e27b1cd1acfb4b293e278c3a89"
|
||||||
|
integrity sha512-nJWqw3bTFy21hX/CPKHth6sfhZbdiHP6bTawSgQBlKOVRG7EZkfHbbHwQJnrE4vsQf0CMNE+3gJ4Fmm16vdVlQ==
|
||||||
|
|
||||||
|
redis-errors@^1.0.0, redis-errors@^1.2.0:
|
||||||
|
version "1.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/redis-errors/-/redis-errors-1.2.0.tgz#eb62d2adb15e4eaf4610c04afe1529384250abad"
|
||||||
|
integrity sha1-62LSrbFeTq9GEMBK/hUpOEJQq60=
|
||||||
|
|
||||||
|
redis-parser@^3.0.0:
|
||||||
|
version "3.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/redis-parser/-/redis-parser-3.0.0.tgz#b66d828cdcafe6b4b8a428a7def4c6bcac31c8b4"
|
||||||
|
integrity sha1-tm2CjNyv5rS4pCin3vTGvKwxyLQ=
|
||||||
|
dependencies:
|
||||||
|
redis-errors "^1.0.0"
|
||||||
|
|
||||||
|
redis@^3.1.2:
|
||||||
|
version "3.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/redis/-/redis-3.1.2.tgz#766851117e80653d23e0ed536254677ab647638c"
|
||||||
|
integrity sha512-grn5KoZLr/qrRQVwoSkmzdbw6pwF+/rwODtrOr6vuBRiR/f3rjSTGupbF90Zpqm2oenix8Do6RV7pYEkGwlKkw==
|
||||||
|
dependencies:
|
||||||
|
denque "^1.5.0"
|
||||||
|
redis-commands "^1.7.0"
|
||||||
|
redis-errors "^1.2.0"
|
||||||
|
redis-parser "^3.0.0"
|
||||||
|
|
||||||
regex-not@^1.0.0, regex-not@^1.0.2:
|
regex-not@^1.0.0, regex-not@^1.0.2:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c"
|
resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c"
|
||||||
|
@ -22,6 +22,7 @@
|
|||||||
"JITSI_URL": env.JITSI_URL,
|
"JITSI_URL": env.JITSI_URL,
|
||||||
"SECRET_JITSI_KEY": env.SECRET_JITSI_KEY,
|
"SECRET_JITSI_KEY": env.SECRET_JITSI_KEY,
|
||||||
"TURN_STATIC_AUTH_SECRET": env.TURN_STATIC_AUTH_SECRET,
|
"TURN_STATIC_AUTH_SECRET": env.TURN_STATIC_AUTH_SECRET,
|
||||||
|
"REDIS_HOST": "redis",
|
||||||
} + (if adminUrl != null then {
|
} + (if adminUrl != null then {
|
||||||
"ADMIN_API_URL": adminUrl,
|
"ADMIN_API_URL": adminUrl,
|
||||||
} else {})
|
} else {})
|
||||||
@ -40,6 +41,7 @@
|
|||||||
"JITSI_URL": env.JITSI_URL,
|
"JITSI_URL": env.JITSI_URL,
|
||||||
"SECRET_JITSI_KEY": env.SECRET_JITSI_KEY,
|
"SECRET_JITSI_KEY": env.SECRET_JITSI_KEY,
|
||||||
"TURN_STATIC_AUTH_SECRET": env.TURN_STATIC_AUTH_SECRET,
|
"TURN_STATIC_AUTH_SECRET": env.TURN_STATIC_AUTH_SECRET,
|
||||||
|
"REDIS_HOST": "redis",
|
||||||
} + (if adminUrl != null then {
|
} + (if adminUrl != null then {
|
||||||
"ADMIN_API_URL": adminUrl,
|
"ADMIN_API_URL": adminUrl,
|
||||||
} else {})
|
} else {})
|
||||||
@ -97,6 +99,9 @@
|
|||||||
},
|
},
|
||||||
"ports": [80]
|
"ports": [80]
|
||||||
},
|
},
|
||||||
|
"redis": {
|
||||||
|
"image": "redis:6",
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
k8sextension(k8sConf)::
|
k8sextension(k8sConf)::
|
||||||
|
@ -120,6 +120,8 @@ services:
|
|||||||
JITSI_URL: $JITSI_URL
|
JITSI_URL: $JITSI_URL
|
||||||
JITSI_ISS: $JITSI_ISS
|
JITSI_ISS: $JITSI_ISS
|
||||||
MAX_PER_GROUP: "$MAX_PER_GROUP"
|
MAX_PER_GROUP: "$MAX_PER_GROUP"
|
||||||
|
REDIS_HOST: redis
|
||||||
|
NODE_ENV: development
|
||||||
volumes:
|
volumes:
|
||||||
- ./back:/usr/src/app
|
- ./back:/usr/src/app
|
||||||
labels:
|
labels:
|
||||||
@ -168,6 +170,9 @@ services:
|
|||||||
- ./front:/usr/src/front
|
- ./front:/usr/src/front
|
||||||
- ./pusher:/usr/src/pusher
|
- ./pusher:/usr/src/pusher
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis:6
|
||||||
|
|
||||||
# coturn:
|
# coturn:
|
||||||
# image: coturn/coturn:4.5.2
|
# image: coturn/coturn:4.5.2
|
||||||
# command:
|
# command:
|
||||||
|
@ -115,6 +115,8 @@ services:
|
|||||||
JITSI_ISS: $JITSI_ISS
|
JITSI_ISS: $JITSI_ISS
|
||||||
TURN_STATIC_AUTH_SECRET: SomeStaticAuthSecret
|
TURN_STATIC_AUTH_SECRET: SomeStaticAuthSecret
|
||||||
MAX_PER_GROUP: "MAX_PER_GROUP"
|
MAX_PER_GROUP: "MAX_PER_GROUP"
|
||||||
|
REDIS_HOST: redis
|
||||||
|
NODE_ENV: development
|
||||||
volumes:
|
volumes:
|
||||||
- ./back:/usr/src/app
|
- ./back:/usr/src/app
|
||||||
labels:
|
labels:
|
||||||
@ -157,6 +159,20 @@ services:
|
|||||||
- ./front:/usr/src/front
|
- ./front:/usr/src/front
|
||||||
- ./pusher:/usr/src/pusher
|
- ./pusher:/usr/src/pusher
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis:6
|
||||||
|
|
||||||
|
redisinsight:
|
||||||
|
image: redislabs/redisinsight:latest
|
||||||
|
labels:
|
||||||
|
- "traefik.http.routers.redisinsight.rule=Host(`redis.workadventure.localhost`)"
|
||||||
|
- "traefik.http.routers.redisinsight.entryPoints=web"
|
||||||
|
- "traefik.http.services.redisinsight.loadbalancer.server.port=8001"
|
||||||
|
- "traefik.http.routers.redisinsight-ssl.rule=Host(`redis.workadventure.localhost`)"
|
||||||
|
- "traefik.http.routers.redisinsight-ssl.entryPoints=websecure"
|
||||||
|
- "traefik.http.routers.redisinsight-ssl.tls=true"
|
||||||
|
- "traefik.http.routers.redisinsight-ssl.service=redisinsight"
|
||||||
|
|
||||||
# coturn:
|
# coturn:
|
||||||
# image: coturn/coturn:4.5.2
|
# image: coturn/coturn:4.5.2
|
||||||
# command:
|
# command:
|
||||||
|
@ -1,6 +1,62 @@
|
|||||||
{.section-title.accent.text-primary}
|
{.section-title.accent.text-primary}
|
||||||
# API Player functions Reference
|
# 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
|
### Listen to player movement
|
||||||
```
|
```
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
{.section-title.accent.text-primary}
|
{.section-title.accent.text-primary}
|
||||||
# API Reference
|
# API Reference
|
||||||
|
|
||||||
|
- [Start / Init functions](api-start.md)
|
||||||
- [Navigation functions](api-nav.md)
|
- [Navigation functions](api-nav.md)
|
||||||
- [Chat functions](api-chat.md)
|
- [Chat functions](api-chat.md)
|
||||||
- [Room functions](api-room.md)
|
- [Room functions](api-room.md)
|
||||||
|
- [State related functions](api-state.md)
|
||||||
- [Player functions](api-player.md)
|
- [Player functions](api-player.md)
|
||||||
- [UI functions](api-ui.md)
|
- [UI functions](api-ui.md)
|
||||||
- [Sound functions](api-sound.md)
|
- [Sound functions](api-sound.md)
|
||||||
|
@ -79,6 +79,58 @@ Example :
|
|||||||
WA.room.setProperty('wikiLayer', 'openWebsite', 'https://www.wikipedia.org/');
|
WA.room.setProperty('wikiLayer', 'openWebsite', 'https://www.wikipedia.org/');
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Get the room id
|
||||||
|
|
||||||
|
```
|
||||||
|
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: 'https://play.workadventu.re/@/myorg/myworld/myroom', or 'https://play.workadventu.re/_/global/mymap.org/map.json"
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Get the map URL
|
||||||
|
|
||||||
|
```
|
||||||
|
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.getTiledMap(): Promise<ITiledMap>
|
||||||
|
```
|
||||||
|
|
||||||
|
Returns a promise that resolves to the JSON map file.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const map = await WA.room.getTiledMap();
|
||||||
|
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
|
### Changing tiles
|
||||||
```
|
```
|
||||||
WA.room.setTiles(tiles: TileDescriptor[]): void
|
WA.room.setTiles(tiles: TileDescriptor[]): void
|
||||||
|
30
docs/maps/api-start.md
Normal file
30
docs/maps/api-start.md
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
{.section-title.accent.text-primary}
|
||||||
|
# API start functions Reference
|
||||||
|
|
||||||
|
### Waiting for WorkAdventure API to be available
|
||||||
|
|
||||||
|
When your script / iFrame loads WorkAdventure, it takes a few milliseconds for your script / iFrame to exchange
|
||||||
|
data with WorkAdventure. You should wait for the WorkAdventure API to be fully ready using the `WA.onInit()` method.
|
||||||
|
|
||||||
|
```
|
||||||
|
WA.onInit(): Promise<void>
|
||||||
|
```
|
||||||
|
|
||||||
|
Some properties (like the current user name, or the room ID) are not available until `WA.onInit` has completed.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
WA.onInit().then(() => {
|
||||||
|
console.log('Current player name: ', WA.player.name);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Or the same code, using await/async:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
(async () => {
|
||||||
|
await WA.onInit();
|
||||||
|
console.log('Current player name: ', WA.player.name);
|
||||||
|
})();
|
||||||
|
```
|
136
docs/maps/api-state.md
Normal file
136
docs/maps/api-state.md
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
{.section-title.accent.text-primary}
|
||||||
|
# API state related functions Reference
|
||||||
|
|
||||||
|
## Saving / loading state
|
||||||
|
|
||||||
|
The `WA.state` functions allow you to easily share a common state between all the players in a given room.
|
||||||
|
Moreover, `WA.state` functions can be used to persist this state across reloads.
|
||||||
|
|
||||||
|
```
|
||||||
|
WA.state.saveVariable(key : string, data : unknown): void
|
||||||
|
WA.state.loadVariable(key : string) : unknown
|
||||||
|
WA.state.onVariableChange(key : string).subscribe((data: unknown) => {}) : Subscription
|
||||||
|
WA.state.[any property]: unknown
|
||||||
|
```
|
||||||
|
|
||||||
|
These methods and properties can be used to save, load and track changes in variables related to the current room.
|
||||||
|
|
||||||
|
Variables stored in `WA.state` can be any value that is serializable in JSON.
|
||||||
|
|
||||||
|
Please refrain from storing large amounts of data in a room. Those functions are typically useful for saving or restoring
|
||||||
|
configuration / metadata.
|
||||||
|
|
||||||
|
{.alert.alert-warning}
|
||||||
|
We are in the process of fine-tuning variables, and we will eventually put limits on the maximum size a variable can hold. We will also put limits on the number of calls you can make to saving variables, so don't change the value of a variable every 10ms, this will fail in the future.
|
||||||
|
|
||||||
|
|
||||||
|
Example :
|
||||||
|
```javascript
|
||||||
|
WA.state.saveVariable('config', {
|
||||||
|
'bottomExitUrl': '/@/org/world/castle',
|
||||||
|
'topExitUrl': '/@/org/world/tower',
|
||||||
|
'enableBirdSound': true
|
||||||
|
}).catch(e => console.error('Something went wrong while saving variable', e));
|
||||||
|
//...
|
||||||
|
let config = WA.state.loadVariable('config');
|
||||||
|
```
|
||||||
|
|
||||||
|
You can use the shortcut properties to load and save variables. The code above is similar to:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
WA.state.config = {
|
||||||
|
'bottomExitUrl': '/@/org/world/castle',
|
||||||
|
'topExitUrl': '/@/org/world/tower',
|
||||||
|
'enableBirdSound': true
|
||||||
|
};
|
||||||
|
|
||||||
|
//...
|
||||||
|
let config = WA.state.config;
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: `saveVariable` returns a promise that will fail in case the variable cannot be saved. This
|
||||||
|
can happen if your user does not have the required rights (more on that in the next chapter).
|
||||||
|
In contrast, if you use the WA.state properties, you cannot access the promise and therefore cannot
|
||||||
|
know for sure if your variable was properly saved.
|
||||||
|
|
||||||
|
If you are using Typescript, please note that the type of variables is `unknown`. This is
|
||||||
|
for security purpose, as we don't know the type of the variable. In order to use the returned value,
|
||||||
|
you will need to cast it to the correct type (or better, use a [Type guard](https://www.typescriptlang.org/docs/handbook/2/narrowing.html) to actually check at runtime
|
||||||
|
that you get the expected type).
|
||||||
|
|
||||||
|
{.alert.alert-warning}
|
||||||
|
For security reasons, the list of variables you are allowed to access and modify is **restricted** (otherwise, anyone on your map could set any data).
|
||||||
|
Variables storage is subject to an authorization process. Read below to learn more.
|
||||||
|
|
||||||
|
### Declaring allowed keys
|
||||||
|
|
||||||
|
In order to declare allowed keys related to a room, you need to add **objects** in an "object layer" of the map.
|
||||||
|
|
||||||
|
Each object will represent a variable.
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<img src="https://workadventu.re/img/docs/object_variable.png" class="figure-img img-fluid rounded" alt="" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
The name of the variable is the name of the object.
|
||||||
|
The object **type** MUST be **variable**.
|
||||||
|
|
||||||
|
You can set a default value for the object in the `default` property.
|
||||||
|
|
||||||
|
### Persisting variables state
|
||||||
|
|
||||||
|
Use the `persist` property to save the state of the variable in database. If `persist` is false, the variable will stay
|
||||||
|
in the memory of the WorkAdventure servers but will be wiped out of the memory as soon as the room is empty (or if the
|
||||||
|
server restarts).
|
||||||
|
|
||||||
|
{.alert.alert-info}
|
||||||
|
Do not use `persist` for highly dynamic values that have a short life spawn.
|
||||||
|
|
||||||
|
### Managing access rights to variables
|
||||||
|
|
||||||
|
With `readableBy` and `writableBy`, you control who can read of write in this variable. The property accepts a string
|
||||||
|
representing a "tag". Anyone having this "tag" can read/write in the variable.
|
||||||
|
|
||||||
|
{.alert.alert-warning}
|
||||||
|
`readableBy` and `writableBy` are specific to the "online" version of WorkAdventure because the notion of tags
|
||||||
|
is not available unless you have an "admin" server (that is not part of the self-hosted version of WorkAdventure).
|
||||||
|
|
||||||
|
Finally, the `jsonSchema` property can contain [a complete JSON schema](https://json-schema.org/) to validate the content of the variable.
|
||||||
|
Trying to set a variable to a value that is not compatible with the schema will fail.
|
||||||
|
|
||||||
|
|
||||||
|
## Tracking variables changes
|
||||||
|
|
||||||
|
The properties of the `WA.state` object are shared in real-time between users of a same room. You can listen to modifications
|
||||||
|
of any property of `WA.state` by using the `WA.state.onVariableChange()` method.
|
||||||
|
|
||||||
|
```
|
||||||
|
WA.state.onVariableChange(name: string): Observable<unknown>
|
||||||
|
```
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
WA.state.onVariableChange('config').subscribe((value) => {
|
||||||
|
console.log('Variable "config" changed. New value: ', value);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
The `WA.state.onVariableChange` method returns an [RxJS `Observable` object](https://rxjs.dev/guide/observable). This is
|
||||||
|
an object on which you can add subscriptions using the `subscribe` method.
|
||||||
|
|
||||||
|
### Stopping tracking variables
|
||||||
|
|
||||||
|
If you want to stop tracking a variable change, the `subscribe` method returns a subscription object with an `unsubscribe` method.
|
||||||
|
|
||||||
|
**Example with unsubscription:**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const subscription = WA.state.onVariableChange('config').subscribe((value) => {
|
||||||
|
console.log('Variable "config" changed. New value: ', value);
|
||||||
|
});
|
||||||
|
// Later:
|
||||||
|
subscription.unsubscribe();
|
||||||
|
```
|
@ -4,10 +4,11 @@ export const isGameStateEvent = new tg.IsInterface()
|
|||||||
.withProperties({
|
.withProperties({
|
||||||
roomId: tg.isString,
|
roomId: tg.isString,
|
||||||
mapUrl: tg.isString,
|
mapUrl: tg.isString,
|
||||||
nickname: tg.isUnion(tg.isString, tg.isNull),
|
nickname: tg.isString,
|
||||||
uuid: tg.isUnion(tg.isString, tg.isUndefined),
|
uuid: tg.isUnion(tg.isString, tg.isUndefined),
|
||||||
startLayerName: tg.isUnion(tg.isString, tg.isNull),
|
startLayerName: tg.isUnion(tg.isString, tg.isNull),
|
||||||
tags: tg.isArray(tg.isString),
|
tags: tg.isArray(tg.isString),
|
||||||
|
variables: tg.isObject,
|
||||||
})
|
})
|
||||||
.get();
|
.get();
|
||||||
/**
|
/**
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import * as tg from "generic-type-guard";
|
||||||
import type { GameStateEvent } from "./GameStateEvent";
|
import type { GameStateEvent } from "./GameStateEvent";
|
||||||
import type { ButtonClickedEvent } from "./ButtonClickedEvent";
|
import type { ButtonClickedEvent } from "./ButtonClickedEvent";
|
||||||
import type { ChatEvent } from "./ChatEvent";
|
import type { ChatEvent } from "./ChatEvent";
|
||||||
@ -9,7 +10,7 @@ import type { OpenCoWebSiteEvent } from "./OpenCoWebSiteEvent";
|
|||||||
import type { OpenPopupEvent } from "./OpenPopupEvent";
|
import type { OpenPopupEvent } from "./OpenPopupEvent";
|
||||||
import type { OpenTabEvent } from "./OpenTabEvent";
|
import type { OpenTabEvent } from "./OpenTabEvent";
|
||||||
import type { UserInputChatEvent } from "./UserInputChatEvent";
|
import type { UserInputChatEvent } from "./UserInputChatEvent";
|
||||||
import type { DataLayerEvent } from "./DataLayerEvent";
|
import type { MapDataEvent } from "./MapDataEvent";
|
||||||
import type { LayerEvent } from "./LayerEvent";
|
import type { LayerEvent } from "./LayerEvent";
|
||||||
import type { SetPropertyEvent } from "./setPropertyEvent";
|
import type { SetPropertyEvent } from "./setPropertyEvent";
|
||||||
import type { LoadSoundEvent } from "./LoadSoundEvent";
|
import type { LoadSoundEvent } from "./LoadSoundEvent";
|
||||||
@ -18,6 +19,10 @@ 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 { HasPlayerMovedEvent } from "./HasPlayerMovedEvent";
|
||||||
import type { SetTilesEvent } from "./SetTilesEvent";
|
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 {
|
export interface TypedMessageEvent<T> extends MessageEvent {
|
||||||
data: T;
|
data: T;
|
||||||
@ -43,7 +48,6 @@ export type IframeEventMap = {
|
|||||||
showLayer: LayerEvent;
|
showLayer: LayerEvent;
|
||||||
hideLayer: LayerEvent;
|
hideLayer: LayerEvent;
|
||||||
setProperty: SetPropertyEvent;
|
setProperty: SetPropertyEvent;
|
||||||
getDataLayer: undefined;
|
|
||||||
loadSound: LoadSoundEvent;
|
loadSound: LoadSoundEvent;
|
||||||
playSound: PlaySoundEvent;
|
playSound: PlaySoundEvent;
|
||||||
stopSound: null;
|
stopSound: null;
|
||||||
@ -66,8 +70,8 @@ export interface IframeResponseEventMap {
|
|||||||
leaveEvent: EnterLeaveEvent;
|
leaveEvent: EnterLeaveEvent;
|
||||||
buttonClickedEvent: ButtonClickedEvent;
|
buttonClickedEvent: ButtonClickedEvent;
|
||||||
hasPlayerMoved: HasPlayerMovedEvent;
|
hasPlayerMoved: HasPlayerMovedEvent;
|
||||||
dataLayer: DataLayerEvent;
|
|
||||||
menuItemClicked: MenuItemClickedEvent;
|
menuItemClicked: MenuItemClickedEvent;
|
||||||
|
setVariable: SetVariableEvent;
|
||||||
}
|
}
|
||||||
export interface IframeResponseEvent<T extends keyof IframeResponseEventMap> {
|
export interface IframeResponseEvent<T extends keyof IframeResponseEventMap> {
|
||||||
type: T;
|
type: T;
|
||||||
@ -81,13 +85,33 @@ export const isIframeResponseEventWrapper = (event: {
|
|||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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: {
|
getState: {
|
||||||
query: undefined,
|
query: tg.isUndefined,
|
||||||
answer: GameStateEvent
|
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> {
|
export interface IframeQuery<T extends keyof IframeQueryMap> {
|
||||||
@ -100,8 +124,21 @@ export interface IframeQueryWrapper<T extends keyof IframeQueryMap> {
|
|||||||
query: IframeQuery<T>;
|
query: IframeQuery<T>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const isIframeQueryKey = (type: string): type is keyof IframeQueryMap => {
|
||||||
|
return type in iframeQueryMapTypeGuards;
|
||||||
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// 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 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
|
// 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 isIframeQueryWrapper = (event: any): event is IframeQueryWrapper<keyof IframeQueryMap> => typeof event.id === 'number' && isIframeQuery(event.query);
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import * as tg from "generic-type-guard";
|
import * as tg from "generic-type-guard";
|
||||||
|
|
||||||
export const isDataLayerEvent = new tg.IsInterface()
|
export const isMapDataEvent = new tg.IsInterface()
|
||||||
.withProperties({
|
.withProperties({
|
||||||
data: tg.isObject,
|
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
|
* 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>;
|
18
front/src/Api/Events/SetVariableEvent.ts
Normal file
18
front/src/Api/Events/SetVariableEvent.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import * as tg from "generic-type-guard";
|
||||||
|
import {isMenuItemRegisterEvent} from "./ui/MenuItemRegisterEvent";
|
||||||
|
|
||||||
|
export const isSetVariableEvent =
|
||||||
|
new tg.IsInterface().withProperties({
|
||||||
|
key: tg.isString,
|
||||||
|
value: tg.isUnknown,
|
||||||
|
}).get();
|
||||||
|
/**
|
||||||
|
* A message sent from the iFrame to the game to change the value of the property of the layer
|
||||||
|
*/
|
||||||
|
export type SetVariableEvent = tg.GuardedType<typeof isSetVariableEvent>;
|
||||||
|
|
||||||
|
export const isSetVariableIframeEvent =
|
||||||
|
new tg.IsInterface().withProperties({
|
||||||
|
type: tg.isSingletonString("setVariable"),
|
||||||
|
data: isSetVariableEvent
|
||||||
|
}).get();
|
@ -26,20 +26,24 @@ import { isLoadSoundEvent, LoadSoundEvent } from "./Events/LoadSoundEvent";
|
|||||||
import { isSetPropertyEvent, SetPropertyEvent } from "./Events/setPropertyEvent";
|
import { isSetPropertyEvent, SetPropertyEvent } from "./Events/setPropertyEvent";
|
||||||
import { isLayerEvent, LayerEvent } from "./Events/LayerEvent";
|
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 { MapDataEvent } from "./Events/MapDataEvent";
|
||||||
import type { GameStateEvent } from "./Events/GameStateEvent";
|
import type { GameStateEvent } from "./Events/GameStateEvent";
|
||||||
import type { HasPlayerMovedEvent } from "./Events/HasPlayerMovedEvent";
|
import type { HasPlayerMovedEvent } from "./Events/HasPlayerMovedEvent";
|
||||||
import { isLoadPageEvent } from "./Events/LoadPageEvent";
|
import { isLoadPageEvent } from "./Events/LoadPageEvent";
|
||||||
import { handleMenuItemRegistrationEvent, isMenuItemRegisterIframeEvent } from "./Events/ui/MenuItemRegisterEvent";
|
import { handleMenuItemRegistrationEvent, isMenuItemRegisterIframeEvent } from "./Events/ui/MenuItemRegisterEvent";
|
||||||
import { SetTilesEvent, isSetTilesEvent } from "./Events/SetTilesEvent";
|
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.
|
* Listens to messages from iframes and turn those messages into easy to use observables.
|
||||||
* Also allows to send messages to those iframes.
|
* Also allows to send messages to those iframes.
|
||||||
*/
|
*/
|
||||||
class IframeListener {
|
class IframeListener {
|
||||||
|
private readonly _readyStream: Subject<HTMLIFrameElement> = new Subject();
|
||||||
|
public readonly readyStream = this._readyStream.asObservable();
|
||||||
|
|
||||||
private readonly _chatStream: Subject<ChatEvent> = new Subject();
|
private readonly _chatStream: Subject<ChatEvent> = new Subject();
|
||||||
public readonly chatStream = this._chatStream.asObservable();
|
public readonly chatStream = this._chatStream.asObservable();
|
||||||
|
|
||||||
@ -85,9 +89,6 @@ class IframeListener {
|
|||||||
private readonly _setPropertyStream: Subject<SetPropertyEvent> = new Subject();
|
private readonly _setPropertyStream: Subject<SetPropertyEvent> = new Subject();
|
||||||
public readonly setPropertyStream = this._setPropertyStream.asObservable();
|
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();
|
private readonly _registerMenuCommandStream: Subject<string> = new Subject();
|
||||||
public readonly registerMenuCommandStream = this._registerMenuCommandStream.asObservable();
|
public readonly registerMenuCommandStream = this._registerMenuCommandStream.asObservable();
|
||||||
|
|
||||||
@ -111,10 +112,13 @@ class IframeListener {
|
|||||||
private readonly scripts = new Map<string, HTMLIFrameElement>();
|
private readonly scripts = new Map<string, HTMLIFrameElement>();
|
||||||
private sendPlayerMove: boolean = false;
|
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: {
|
private answerers: {
|
||||||
[key in keyof IframeQueryMap]?: AnswererCallback<key>
|
[str in keyof IframeQueryMap]?: unknown
|
||||||
} = {};
|
} = {};
|
||||||
|
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
window.addEventListener(
|
window.addEventListener(
|
||||||
"message",
|
"message",
|
||||||
@ -152,7 +156,7 @@ class IframeListener {
|
|||||||
const queryId = payload.id;
|
const queryId = payload.id;
|
||||||
const query = payload.query;
|
const query = payload.query;
|
||||||
|
|
||||||
const answerer = this.answerers[query.type];
|
const answerer = this.answerers[query.type] as AnswererCallback<keyof IframeQueryMap> | undefined;
|
||||||
if (answerer === 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);
|
console.error(errorMsg);
|
||||||
@ -164,19 +168,15 @@ class IframeListener {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Promise.resolve(answerer(query.data)).then((value) => {
|
const errorHandler = (reason: unknown) => {
|
||||||
iframe?.contentWindow?.postMessage({
|
|
||||||
id: queryId,
|
|
||||||
type: query.type,
|
|
||||||
data: value
|
|
||||||
}, '*');
|
|
||||||
}).catch(reason => {
|
|
||||||
console.error('An error occurred while responding to an iFrame query.', reason);
|
console.error('An error occurred while responding to an iFrame query.', reason);
|
||||||
let reasonMsg: string;
|
let reasonMsg: string = '';
|
||||||
if (reason instanceof Error) {
|
if (reason instanceof Error) {
|
||||||
reasonMsg = reason.message;
|
reasonMsg = reason.message;
|
||||||
} else {
|
} else if (typeof reason === 'object') {
|
||||||
reasonMsg = reason.toString();
|
reasonMsg = reason ? reason.toString() : '';
|
||||||
|
} else if (typeof reason === 'string') {
|
||||||
|
reasonMsg = reason;
|
||||||
}
|
}
|
||||||
|
|
||||||
iframe?.contentWindow?.postMessage({
|
iframe?.contentWindow?.postMessage({
|
||||||
@ -184,8 +184,31 @@ class IframeListener {
|
|||||||
type: query.type,
|
type: query.type,
|
||||||
error: reasonMsg
|
error: reasonMsg
|
||||||
} as IframeErrorAnswerEvent, '*');
|
} as IframeErrorAnswerEvent, '*');
|
||||||
});
|
};
|
||||||
|
|
||||||
|
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)) {
|
} else if (isIframeEventWrapper(payload)) {
|
||||||
if (payload.type === "showLayer" && isLayerEvent(payload.data)) {
|
if (payload.type === "showLayer" && isLayerEvent(payload.data)) {
|
||||||
this._showLayerStream.next(payload.data);
|
this._showLayerStream.next(payload.data);
|
||||||
@ -230,8 +253,6 @@ class IframeListener {
|
|||||||
this._removeBubbleStream.next();
|
this._removeBubbleStream.next();
|
||||||
} else if (payload.type == "onPlayerMove") {
|
} else if (payload.type == "onPlayerMove") {
|
||||||
this.sendPlayerMove = true;
|
this.sendPlayerMove = true;
|
||||||
} else if (payload.type == "getDataLayer") {
|
|
||||||
this._dataLayerChangeStream.next();
|
|
||||||
} else if (isMenuItemRegisterIframeEvent(payload)) {
|
} else if (isMenuItemRegisterIframeEvent(payload)) {
|
||||||
const data = payload.data.menutItem;
|
const data = payload.data.menutItem;
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@ -248,13 +269,6 @@ class IframeListener {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
sendDataLayerEvent(dataLayerEvent: DataLayerEvent) {
|
|
||||||
this.postMessage({
|
|
||||||
type: "dataLayer",
|
|
||||||
data: dataLayerEvent,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Allows the passed iFrame to send/receive messages via the API.
|
* Allows the passed iFrame to send/receive messages via the API.
|
||||||
*/
|
*/
|
||||||
@ -394,6 +408,13 @@ class IframeListener {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setVariable(setVariableEvent: SetVariableEvent) {
|
||||||
|
this.postMessage({
|
||||||
|
'type': 'setVariable',
|
||||||
|
'data': setVariableEvent
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends the message... to all allowed iframes.
|
* Sends the message... to all allowed iframes.
|
||||||
*/
|
*/
|
||||||
@ -411,7 +432,7 @@ class IframeListener {
|
|||||||
* @param key The "type" of the query we are answering
|
* @param key The "type" of the query we are answering
|
||||||
* @param callback
|
* @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;
|
this.answerers[key] = callback;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,17 +2,28 @@ import { IframeApiContribution, sendToWorkadventure } from "./IframeApiContribut
|
|||||||
import type { HasPlayerMovedEvent, HasPlayerMovedEventCallback } from "../Events/HasPlayerMovedEvent";
|
import type { HasPlayerMovedEvent, HasPlayerMovedEventCallback } from "../Events/HasPlayerMovedEvent";
|
||||||
import { Subject } from "rxjs";
|
import { Subject } from "rxjs";
|
||||||
import { apiCallback } from "./registeredCallbacks";
|
import { apiCallback } from "./registeredCallbacks";
|
||||||
import { getGameState } from "./room";
|
|
||||||
import { isHasPlayerMovedEvent } from "../Events/HasPlayerMovedEvent";
|
import { isHasPlayerMovedEvent } from "../Events/HasPlayerMovedEvent";
|
||||||
|
|
||||||
interface User {
|
|
||||||
id: string | undefined;
|
|
||||||
nickName: string | null;
|
|
||||||
tags: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
const moveStream = new Subject<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> {
|
export class WorkadventurePlayerCommands extends IframeApiContribution<WorkadventurePlayerCommands> {
|
||||||
callbacks = [
|
callbacks = [
|
||||||
apiCallback({
|
apiCallback({
|
||||||
@ -31,10 +42,30 @@ export class WorkadventurePlayerCommands extends IframeApiContribution<Workadven
|
|||||||
data: null,
|
data: null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
getCurrentUser(): Promise<User> {
|
|
||||||
return getGameState().then((gameState) => {
|
get name(): string {
|
||||||
return { id: gameState.uuid, nickName: gameState.nickname, tags: gameState.tags };
|
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 { EnterLeaveEvent, isEnterLeaveEvent } from "../Events/EnterLeaveEvent";
|
||||||
import { isGameStateEvent } from "../Events/GameStateEvent";
|
|
||||||
|
|
||||||
import { IframeApiContribution, queryWorkadventure, sendToWorkadventure } from "./IframeApiContribution";
|
import { IframeApiContribution, queryWorkadventure, sendToWorkadventure } from "./IframeApiContribution";
|
||||||
import { apiCallback } from "./registeredCallbacks";
|
import { apiCallback } from "./registeredCallbacks";
|
||||||
|
|
||||||
import type { ITiledMap } from "../../Phaser/Map/ITiledMap";
|
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 enterStreams: Map<string, Subject<EnterLeaveEvent>> = new Map<string, Subject<EnterLeaveEvent>>();
|
||||||
const leaveStreams: 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 {
|
interface TileDescriptor {
|
||||||
x: number;
|
x: number;
|
||||||
@ -31,19 +17,17 @@ interface TileDescriptor {
|
|||||||
layer: string;
|
layer: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getGameState(): Promise<GameStateEvent> {
|
let roomId: string | undefined;
|
||||||
if (immutableDataPromise === undefined) {
|
|
||||||
immutableDataPromise = queryWorkadventure({ type: "getState", data: undefined });
|
|
||||||
}
|
|
||||||
return immutableDataPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDataLayer(): Promise<DataLayerEvent> {
|
export const setRoomId = (id: string) => {
|
||||||
return new Promise<DataLayerEvent>((resolver, thrower) => {
|
roomId = id;
|
||||||
dataLayerResolver.subscribe(resolver);
|
};
|
||||||
sendToWorkadventure({ type: "getDataLayer", data: null });
|
|
||||||
});
|
let mapURL: string | undefined;
|
||||||
}
|
|
||||||
|
export const setMapURL = (url: string) => {
|
||||||
|
mapURL = url;
|
||||||
|
};
|
||||||
|
|
||||||
export class WorkadventureRoomCommands extends IframeApiContribution<WorkadventureRoomCommands> {
|
export class WorkadventureRoomCommands extends IframeApiContribution<WorkadventureRoomCommands> {
|
||||||
callbacks = [
|
callbacks = [
|
||||||
@ -61,13 +45,6 @@ export class WorkadventureRoomCommands extends IframeApiContribution<Workadventu
|
|||||||
leaveStreams.get(payloadData.name)?.next();
|
leaveStreams.get(payloadData.name)?.next();
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
apiCallback({
|
|
||||||
type: "dataLayer",
|
|
||||||
typeChecker: isDataLayerEvent,
|
|
||||||
callback: (payloadData) => {
|
|
||||||
dataLayerResolver.next(payloadData);
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
onEnterZone(name: string, callback: () => void): void {
|
onEnterZone(name: string, callback: () => void): void {
|
||||||
@ -102,17 +79,9 @@ export class WorkadventureRoomCommands extends IframeApiContribution<Workadventu
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
getCurrentRoom(): Promise<Room> {
|
async getTiledMap(): Promise<ITiledMap> {
|
||||||
return getGameState().then((gameState) => {
|
const event = await queryWorkadventure({ type: "getMapData", data: undefined });
|
||||||
return getDataLayer().then((mapJson) => {
|
return event.data as ITiledMap;
|
||||||
return {
|
|
||||||
id: gameState.roomId,
|
|
||||||
map: mapJson.data as ITiledMap,
|
|
||||||
mapUrl: gameState.mapUrl,
|
|
||||||
startLayer: gameState.startLayerName,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
setTiles(tiles: TileDescriptor[]) {
|
setTiles(tiles: TileDescriptor[]) {
|
||||||
sendToWorkadventure({
|
sendToWorkadventure({
|
||||||
@ -120,6 +89,22 @@ export class WorkadventureRoomCommands extends IframeApiContribution<Workadventu
|
|||||||
data: tiles,
|
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();
|
export default new WorkadventureRoomCommands();
|
||||||
|
92
front/src/Api/iframe/state.ts
Normal file
92
front/src/Api/iframe/state.ts
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
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;
|
@ -1,89 +1,107 @@
|
|||||||
import Axios from "axios";
|
import Axios from "axios";
|
||||||
import {PUSHER_URL, START_ROOM_URL} from "../Enum/EnvironmentVariable";
|
import { PUSHER_URL, START_ROOM_URL } from "../Enum/EnvironmentVariable";
|
||||||
import {RoomConnection} from "./RoomConnection";
|
import { RoomConnection } from "./RoomConnection";
|
||||||
import type {OnConnectInterface, PositionInterface, ViewportInterface} from "./ConnexionModels";
|
import type { OnConnectInterface, PositionInterface, ViewportInterface } from "./ConnexionModels";
|
||||||
import {GameConnexionTypes, urlManager} from "../Url/UrlManager";
|
import { GameConnexionTypes, urlManager } from "../Url/UrlManager";
|
||||||
import {localUserStore} from "./LocalUserStore";
|
import { localUserStore } from "./LocalUserStore";
|
||||||
import {CharacterTexture, LocalUser} from "./LocalUser";
|
import { CharacterTexture, LocalUser } from "./LocalUser";
|
||||||
import {Room} from "./Room";
|
import { Room } from "./Room";
|
||||||
|
|
||||||
|
|
||||||
class ConnectionManager {
|
class ConnectionManager {
|
||||||
private localUser!:LocalUser;
|
private localUser!: LocalUser;
|
||||||
|
|
||||||
private connexionType?: GameConnexionTypes
|
private connexionType?: GameConnexionTypes;
|
||||||
private reconnectingTimeout: NodeJS.Timeout|null = null;
|
private reconnectingTimeout: NodeJS.Timeout | null = null;
|
||||||
private _unloading:boolean = false;
|
private _unloading: boolean = false;
|
||||||
|
|
||||||
get unloading () {
|
get unloading() {
|
||||||
return this._unloading;
|
return this._unloading;
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
window.addEventListener('beforeunload', () => {
|
window.addEventListener("beforeunload", () => {
|
||||||
this._unloading = true;
|
this._unloading = true;
|
||||||
if (this.reconnectingTimeout) clearTimeout(this.reconnectingTimeout)
|
if (this.reconnectingTimeout) clearTimeout(this.reconnectingTimeout);
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Tries to login to the node server and return the starting map url to be loaded
|
* Tries to login to the node server and return the starting map url to be loaded
|
||||||
*/
|
*/
|
||||||
public async initGameConnexion(): Promise<Room> {
|
public async initGameConnexion(): Promise<Room> {
|
||||||
|
|
||||||
const connexionType = urlManager.getGameConnexionType();
|
const connexionType = urlManager.getGameConnexionType();
|
||||||
this.connexionType = connexionType;
|
this.connexionType = connexionType;
|
||||||
if(connexionType === GameConnexionTypes.register) {
|
if (connexionType === GameConnexionTypes.register) {
|
||||||
const organizationMemberToken = urlManager.getOrganizationToken();
|
const organizationMemberToken = urlManager.getOrganizationToken();
|
||||||
const data = await Axios.post(`${PUSHER_URL}/register`, {organizationMemberToken}).then(res => res.data);
|
const data = await Axios.post(`${PUSHER_URL}/register`, { organizationMemberToken }).then(
|
||||||
|
(res) => res.data
|
||||||
|
);
|
||||||
this.localUser = new LocalUser(data.userUuid, data.authToken, data.textures);
|
this.localUser = new LocalUser(data.userUuid, data.authToken, data.textures);
|
||||||
localUserStore.saveUser(this.localUser);
|
localUserStore.saveUser(this.localUser);
|
||||||
|
|
||||||
const roomUrl = data.roomUrl;
|
const roomUrl = data.roomUrl;
|
||||||
|
|
||||||
const room = await Room.createRoom(new URL(window.location.protocol + '//' + window.location.host + roomUrl + window.location.search + window.location.hash));
|
const room = await Room.createRoom(
|
||||||
|
new URL(
|
||||||
|
window.location.protocol +
|
||||||
|
"//" +
|
||||||
|
window.location.host +
|
||||||
|
roomUrl +
|
||||||
|
window.location.search +
|
||||||
|
window.location.hash
|
||||||
|
)
|
||||||
|
);
|
||||||
urlManager.pushRoomIdToUrl(room);
|
urlManager.pushRoomIdToUrl(room);
|
||||||
return Promise.resolve(room);
|
return Promise.resolve(room);
|
||||||
} else if (connexionType === GameConnexionTypes.organization || connexionType === GameConnexionTypes.anonymous || connexionType === GameConnexionTypes.empty) {
|
} else if (
|
||||||
|
connexionType === GameConnexionTypes.organization ||
|
||||||
|
connexionType === GameConnexionTypes.anonymous ||
|
||||||
|
connexionType === GameConnexionTypes.empty
|
||||||
|
) {
|
||||||
let localUser = localUserStore.getLocalUser();
|
let localUser = localUserStore.getLocalUser();
|
||||||
if (localUser && localUser.jwtToken && localUser.uuid && localUser.textures) {
|
if (localUser && localUser.jwtToken && localUser.uuid && localUser.textures) {
|
||||||
this.localUser = localUser;
|
this.localUser = localUser;
|
||||||
try {
|
try {
|
||||||
await this.verifyToken(localUser.jwtToken);
|
await this.verifyToken(localUser.jwtToken);
|
||||||
} catch(e) {
|
} catch (e) {
|
||||||
// If the token is invalid, let's generate an anonymous one.
|
// If the token is invalid, let's generate an anonymous one.
|
||||||
console.error('JWT token invalid. Did it expire? Login anonymously instead.');
|
console.error("JWT token invalid. Did it expire? Login anonymously instead.");
|
||||||
await this.anonymousLogin();
|
await this.anonymousLogin();
|
||||||
}
|
}
|
||||||
}else{
|
} else {
|
||||||
await this.anonymousLogin();
|
await this.anonymousLogin();
|
||||||
}
|
}
|
||||||
|
|
||||||
localUser = localUserStore.getLocalUser();
|
localUser = localUserStore.getLocalUser();
|
||||||
if(!localUser){
|
if (!localUser) {
|
||||||
throw "Error to store local user data";
|
throw "Error to store local user data";
|
||||||
}
|
}
|
||||||
|
|
||||||
let roomPath: string;
|
let roomPath: string;
|
||||||
if (connexionType === GameConnexionTypes.empty) {
|
if (connexionType === GameConnexionTypes.empty) {
|
||||||
roomPath = window.location.protocol + '//' + window.location.host + START_ROOM_URL;
|
roomPath = window.location.protocol + "//" + window.location.host + START_ROOM_URL;
|
||||||
} else {
|
} else {
|
||||||
roomPath = window.location.protocol + '//' + window.location.host + window.location.pathname + window.location.search + window.location.hash;
|
roomPath =
|
||||||
|
window.location.protocol +
|
||||||
|
"//" +
|
||||||
|
window.location.host +
|
||||||
|
window.location.pathname +
|
||||||
|
window.location.search +
|
||||||
|
window.location.hash;
|
||||||
}
|
}
|
||||||
|
|
||||||
//get detail map for anonymous login and set texture in local storage
|
//get detail map for anonymous login and set texture in local storage
|
||||||
const room = await Room.createRoom(new URL(roomPath));
|
const room = await Room.createRoom(new URL(roomPath));
|
||||||
if(room.textures != undefined && room.textures.length > 0) {
|
if (room.textures != undefined && room.textures.length > 0) {
|
||||||
//check if texture was changed
|
//check if texture was changed
|
||||||
if(localUser.textures.length === 0){
|
if (localUser.textures.length === 0) {
|
||||||
localUser.textures = room.textures;
|
localUser.textures = room.textures;
|
||||||
}else{
|
} else {
|
||||||
room.textures.forEach((newTexture) => {
|
room.textures.forEach((newTexture) => {
|
||||||
const alreadyExistTexture = localUser?.textures.find((c) => newTexture.id === c.id);
|
const alreadyExistTexture = localUser?.textures.find((c) => newTexture.id === c.id);
|
||||||
if(localUser?.textures.findIndex((c) => newTexture.id === c.id) !== -1){
|
if (localUser?.textures.findIndex((c) => newTexture.id === c.id) !== -1) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
localUser?.textures.push(newTexture)
|
localUser?.textures.push(newTexture);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
this.localUser = localUser;
|
this.localUser = localUser;
|
||||||
@ -92,55 +110,79 @@ class ConnectionManager {
|
|||||||
return Promise.resolve(room);
|
return Promise.resolve(room);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.reject(new Error('Invalid URL'));
|
return Promise.reject(new Error("Invalid URL"));
|
||||||
}
|
}
|
||||||
|
|
||||||
private async verifyToken(token: string): Promise<void> {
|
private async verifyToken(token: string): Promise<void> {
|
||||||
await Axios.get(`${PUSHER_URL}/verify`, {params: {token}});
|
await Axios.get(`${PUSHER_URL}/verify`, { params: { token } });
|
||||||
}
|
}
|
||||||
|
|
||||||
public async anonymousLogin(isBenchmark: boolean = false): Promise<void> {
|
public async anonymousLogin(isBenchmark: boolean = false): Promise<void> {
|
||||||
const data = await Axios.post(`${PUSHER_URL}/anonymLogin`).then(res => res.data);
|
const data = await Axios.post(`${PUSHER_URL}/anonymLogin`).then((res) => res.data);
|
||||||
this.localUser = new LocalUser(data.userUuid, data.authToken, []);
|
this.localUser = new LocalUser(data.userUuid, data.authToken, []);
|
||||||
if (!isBenchmark) { // In benchmark, we don't have a local storage.
|
if (!isBenchmark) {
|
||||||
|
// In benchmark, we don't have a local storage.
|
||||||
localUserStore.saveUser(this.localUser);
|
localUserStore.saveUser(this.localUser);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public initBenchmark(): void {
|
public initBenchmark(): void {
|
||||||
this.localUser = new LocalUser('', 'test', []);
|
this.localUser = new LocalUser("", "test", []);
|
||||||
}
|
}
|
||||||
|
|
||||||
public connectToRoomSocket(roomUrl: string, name: string, characterLayers: string[], position: PositionInterface, viewport: ViewportInterface, companion: string|null): Promise<OnConnectInterface> {
|
public connectToRoomSocket(
|
||||||
|
roomUrl: string,
|
||||||
|
name: string,
|
||||||
|
characterLayers: string[],
|
||||||
|
position: PositionInterface,
|
||||||
|
viewport: ViewportInterface,
|
||||||
|
companion: string | null
|
||||||
|
): Promise<OnConnectInterface> {
|
||||||
return new Promise<OnConnectInterface>((resolve, reject) => {
|
return new Promise<OnConnectInterface>((resolve, reject) => {
|
||||||
const connection = new RoomConnection(this.localUser.jwtToken, roomUrl, name, characterLayers, position, viewport, companion);
|
const connection = new RoomConnection(
|
||||||
|
this.localUser.jwtToken,
|
||||||
|
roomUrl,
|
||||||
|
name,
|
||||||
|
characterLayers,
|
||||||
|
position,
|
||||||
|
viewport,
|
||||||
|
companion
|
||||||
|
);
|
||||||
connection.onConnectError((error: object) => {
|
connection.onConnectError((error: object) => {
|
||||||
console.log('An error occurred while connecting to socket server. Retrying');
|
console.log("An error occurred while connecting to socket server. Retrying");
|
||||||
reject(error);
|
reject(error);
|
||||||
});
|
});
|
||||||
|
|
||||||
connection.onConnectingError((event: CloseEvent) => {
|
connection.onConnectingError((event: CloseEvent) => {
|
||||||
console.log('An error occurred while connecting to socket server. Retrying');
|
console.log("An error occurred while connecting to socket server. Retrying");
|
||||||
reject(new Error('An error occurred while connecting to socket server. Retrying. Code: '+event.code+', Reason: '+event.reason));
|
reject(
|
||||||
|
new Error(
|
||||||
|
"An error occurred while connecting to socket server. Retrying. Code: " +
|
||||||
|
event.code +
|
||||||
|
", Reason: " +
|
||||||
|
event.reason
|
||||||
|
)
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
connection.onConnect((connect: OnConnectInterface) => {
|
connection.onConnect((connect: OnConnectInterface) => {
|
||||||
resolve(connect);
|
resolve(connect);
|
||||||
});
|
});
|
||||||
|
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
// Let's retry in 4-6 seconds
|
// Let's retry in 4-6 seconds
|
||||||
return new Promise<OnConnectInterface>((resolve, reject) => {
|
return new Promise<OnConnectInterface>((resolve, reject) => {
|
||||||
this.reconnectingTimeout = setTimeout(() => {
|
this.reconnectingTimeout = setTimeout(() => {
|
||||||
//todo: allow a way to break recursion?
|
//todo: allow a way to break recursion?
|
||||||
//todo: find a way to avoid recursive function. Otherwise, the call stack will grow indefinitely.
|
//todo: find a way to avoid recursive function. Otherwise, the call stack will grow indefinitely.
|
||||||
this.connectToRoomSocket(roomUrl, name, characterLayers, position, viewport, companion).then((connection) => resolve(connection));
|
this.connectToRoomSocket(roomUrl, name, characterLayers, position, viewport, companion).then(
|
||||||
}, 4000 + Math.floor(Math.random() * 2000) );
|
(connection) => resolve(connection)
|
||||||
|
);
|
||||||
|
}, 4000 + Math.floor(Math.random() * 2000));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
get getConnexionType(){
|
get getConnexionType() {
|
||||||
return this.connexionType;
|
return this.connexionType;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -31,6 +31,7 @@ export enum EventMessage {
|
|||||||
TELEPORT = "teleport",
|
TELEPORT = "teleport",
|
||||||
USER_MESSAGE = "user-message",
|
USER_MESSAGE = "user-message",
|
||||||
START_JITSI_ROOM = "start-jitsi-room",
|
START_JITSI_ROOM = "start-jitsi-room",
|
||||||
|
SET_VARIABLE = "set-variable",
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PointInterface {
|
export interface PointInterface {
|
||||||
@ -105,6 +106,7 @@ export interface RoomJoinedMessageInterface {
|
|||||||
//users: MessageUserPositionInterface[],
|
//users: MessageUserPositionInterface[],
|
||||||
//groups: GroupCreatedUpdatedMessageInterface[],
|
//groups: GroupCreatedUpdatedMessageInterface[],
|
||||||
items: { [itemId: number]: unknown };
|
items: { [itemId: number]: unknown };
|
||||||
|
variables: Map<string, unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PlayGlobalMessageInterface {
|
export interface PlayGlobalMessageInterface {
|
||||||
|
@ -32,6 +32,7 @@ import {
|
|||||||
EmotePromptMessage,
|
EmotePromptMessage,
|
||||||
SendUserMessage,
|
SendUserMessage,
|
||||||
BanUserMessage,
|
BanUserMessage,
|
||||||
|
VariableMessage, ErrorMessage,
|
||||||
} from "../Messages/generated/messages_pb";
|
} from "../Messages/generated/messages_pb";
|
||||||
|
|
||||||
import type { UserSimplePeerInterface } from "../WebRtc/SimplePeer";
|
import type { UserSimplePeerInterface } from "../WebRtc/SimplePeer";
|
||||||
@ -164,6 +165,12 @@ export class RoomConnection implements RoomConnection {
|
|||||||
} else if (subMessage.hasEmoteeventmessage()) {
|
} else if (subMessage.hasEmoteeventmessage()) {
|
||||||
const emoteMessage = subMessage.getEmoteeventmessage() as EmoteEventMessage;
|
const emoteMessage = subMessage.getEmoteeventmessage() as EmoteEventMessage;
|
||||||
emoteEventStream.fire(emoteMessage.getActoruserid(), emoteMessage.getEmote());
|
emoteEventStream.fire(emoteMessage.getActoruserid(), emoteMessage.getEmote());
|
||||||
|
} else if (subMessage.hasErrormessage()) {
|
||||||
|
const errorMessage = subMessage.getErrormessage() as ErrorMessage;
|
||||||
|
console.error('An error occurred server side: '+errorMessage.getMessage());
|
||||||
|
} else if (subMessage.hasVariablemessage()) {
|
||||||
|
event = EventMessage.SET_VARIABLE;
|
||||||
|
payload = subMessage.getVariablemessage();
|
||||||
} else {
|
} else {
|
||||||
throw new Error("Unexpected batch message type");
|
throw new Error("Unexpected batch message type");
|
||||||
}
|
}
|
||||||
@ -180,6 +187,15 @@ export class RoomConnection implements RoomConnection {
|
|||||||
items[item.getItemid()] = JSON.parse(item.getStatejson());
|
items[item.getItemid()] = JSON.parse(item.getStatejson());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const variables = new Map<string, unknown>();
|
||||||
|
for (const variable of roomJoinedMessage.getVariableList()) {
|
||||||
|
try {
|
||||||
|
variables.set(variable.getName(), JSON.parse(variable.getValue()));
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Unable to unserialize value received from server for variable "'+variable.getName()+'". Value received: "'+variable.getValue()+'". Error: ', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.userId = roomJoinedMessage.getCurrentuserid();
|
this.userId = roomJoinedMessage.getCurrentuserid();
|
||||||
this.tags = roomJoinedMessage.getTagList();
|
this.tags = roomJoinedMessage.getTagList();
|
||||||
|
|
||||||
@ -187,6 +203,7 @@ export class RoomConnection implements RoomConnection {
|
|||||||
connection: this,
|
connection: this,
|
||||||
room: {
|
room: {
|
||||||
items,
|
items,
|
||||||
|
variables,
|
||||||
} as RoomJoinedMessageInterface,
|
} as RoomJoinedMessageInterface,
|
||||||
});
|
});
|
||||||
} else if (message.hasWorldfullmessage()) {
|
} else if (message.hasWorldfullmessage()) {
|
||||||
@ -536,6 +553,17 @@ export class RoomConnection implements RoomConnection {
|
|||||||
this.socket.send(clientToServerMessage.serializeBinary().buffer);
|
this.socket.send(clientToServerMessage.serializeBinary().buffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
emitSetVariableEvent(name: string, value: unknown): void {
|
||||||
|
const variableMessage = new VariableMessage();
|
||||||
|
variableMessage.setName(name);
|
||||||
|
variableMessage.setValue(JSON.stringify(value));
|
||||||
|
|
||||||
|
const clientToServerMessage = new ClientToServerMessage();
|
||||||
|
clientToServerMessage.setVariablemessage(variableMessage);
|
||||||
|
|
||||||
|
this.socket.send(clientToServerMessage.serializeBinary().buffer);
|
||||||
|
}
|
||||||
|
|
||||||
onActionableEvent(callback: (message: ItemEventMessageInterface) => void): void {
|
onActionableEvent(callback: (message: ItemEventMessageInterface) => void): void {
|
||||||
this.onMessage(EventMessage.ITEM_EVENT, (message: ItemEventMessage) => {
|
this.onMessage(EventMessage.ITEM_EVENT, (message: ItemEventMessage) => {
|
||||||
callback({
|
callback({
|
||||||
@ -622,6 +650,22 @@ export class RoomConnection implements RoomConnection {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public onSetVariable(callback: (name: string, value: unknown) => void): void {
|
||||||
|
this.onMessage(EventMessage.SET_VARIABLE, (message: VariableMessage) => {
|
||||||
|
const name = message.getName();
|
||||||
|
const serializedValue = message.getValue();
|
||||||
|
let value: unknown = undefined;
|
||||||
|
if (serializedValue) {
|
||||||
|
try {
|
||||||
|
value = JSON.parse(serializedValue);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Unable to unserialize value received from server for variable "'+name+'". Value received: "'+serializedValue+'". Error: ', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
callback(name, value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public hasTag(tag: string): boolean {
|
public hasTag(tag: string): boolean {
|
||||||
return this.tags.includes(tag);
|
return this.tags.includes(tag);
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import type {PointInterface} from "../../Connexion/ConnexionModels";
|
import type { PointInterface } from "../../Connexion/ConnexionModels";
|
||||||
import type {PlayerInterface} from "./PlayerInterface";
|
import type { PlayerInterface } from "./PlayerInterface";
|
||||||
|
|
||||||
export interface AddPlayerInterface extends PlayerInterface {
|
export interface AddPlayerInterface extends PlayerInterface {
|
||||||
position: PointInterface;
|
position: PointInterface;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import type { ITiledMap, ITiledMapLayer, ITiledMapLayerProperty } from "../Map/ITiledMap";
|
import type { ITiledMap, ITiledMapLayer, ITiledMapProperty } from "../Map/ITiledMap";
|
||||||
import { flattenGroupLayersMap } from "../Map/LayersFlattener";
|
import { flattenGroupLayersMap } from "../Map/LayersFlattener";
|
||||||
import TilemapLayer = Phaser.Tilemaps.TilemapLayer;
|
import TilemapLayer = Phaser.Tilemaps.TilemapLayer;
|
||||||
import { DEPTH_OVERLAY_INDEX } from "./DepthIndexes";
|
import { DEPTH_OVERLAY_INDEX } from "./DepthIndexes";
|
||||||
@ -19,7 +19,7 @@ export class GameMap {
|
|||||||
private callbacks = new Map<string, Array<PropertyChangeCallback>>();
|
private callbacks = new Map<string, Array<PropertyChangeCallback>>();
|
||||||
private tileNameMap = new Map<string, number>();
|
private tileNameMap = new Map<string, number>();
|
||||||
|
|
||||||
private tileSetPropertyMap: { [tile_index: number]: Array<ITiledMapLayerProperty> } = {};
|
private tileSetPropertyMap: { [tile_index: number]: Array<ITiledMapProperty> } = {};
|
||||||
public readonly flatLayers: ITiledMapLayer[];
|
public readonly flatLayers: ITiledMapLayer[];
|
||||||
public readonly phaserLayers: TilemapLayer[] = [];
|
public readonly phaserLayers: TilemapLayer[] = [];
|
||||||
|
|
||||||
@ -61,7 +61,7 @@ export class GameMap {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public getPropertiesForIndex(index: number): Array<ITiledMapLayerProperty> {
|
public getPropertiesForIndex(index: number): Array<ITiledMapProperty> {
|
||||||
if (this.tileSetPropertyMap[index]) {
|
if (this.tileSetPropertyMap[index]) {
|
||||||
return this.tileSetPropertyMap[index];
|
return this.tileSetPropertyMap[index];
|
||||||
}
|
}
|
||||||
@ -151,7 +151,7 @@ export class GameMap {
|
|||||||
return this.map;
|
return this.map;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getTileProperty(index: number): Array<ITiledMapLayerProperty> {
|
private getTileProperty(index: number): Array<ITiledMapProperty> {
|
||||||
if (this.tileSetPropertyMap[index]) {
|
if (this.tileSetPropertyMap[index]) {
|
||||||
return this.tileSetPropertyMap[index];
|
return this.tileSetPropertyMap[index];
|
||||||
}
|
}
|
||||||
|
@ -47,13 +47,7 @@ import { RemotePlayer } from "../Entity/RemotePlayer";
|
|||||||
import type { ActionableItem } from "../Items/ActionableItem";
|
import type { ActionableItem } from "../Items/ActionableItem";
|
||||||
import type { ItemFactoryInterface } from "../Items/ItemFactoryInterface";
|
import type { ItemFactoryInterface } from "../Items/ItemFactoryInterface";
|
||||||
import { SelectCharacterScene, SelectCharacterSceneName } from "../Login/SelectCharacterScene";
|
import { SelectCharacterScene, SelectCharacterSceneName } from "../Login/SelectCharacterScene";
|
||||||
import type {
|
import type { ITiledMap, ITiledMapLayer, ITiledMapProperty, ITiledMapObject, ITiledTileSet } from "../Map/ITiledMap";
|
||||||
ITiledMap,
|
|
||||||
ITiledMapLayer,
|
|
||||||
ITiledMapLayerProperty,
|
|
||||||
ITiledMapObject,
|
|
||||||
ITiledTileSet,
|
|
||||||
} from "../Map/ITiledMap";
|
|
||||||
import { MenuScene, MenuSceneName } from "../Menu/MenuScene";
|
import { MenuScene, MenuSceneName } from "../Menu/MenuScene";
|
||||||
import { PlayerAnimationDirections } from "../Player/Animation";
|
import { PlayerAnimationDirections } from "../Player/Animation";
|
||||||
import { hasMovedEventName, Player, requestEmoteEventName } from "../Player/Player";
|
import { hasMovedEventName, Player, requestEmoteEventName } from "../Player/Player";
|
||||||
@ -91,6 +85,7 @@ import { soundManager } from "./SoundManager";
|
|||||||
import { peerStore, screenSharingPeerStore } from "../../Stores/PeerStore";
|
import { peerStore, screenSharingPeerStore } from "../../Stores/PeerStore";
|
||||||
import { videoFocusStore } from "../../Stores/VideoFocusStore";
|
import { videoFocusStore } from "../../Stores/VideoFocusStore";
|
||||||
import { biggestAvailableAreaStore } from "../../Stores/BiggestAvailableAreaStore";
|
import { biggestAvailableAreaStore } from "../../Stores/BiggestAvailableAreaStore";
|
||||||
|
import { SharedVariablesManager } from "./SharedVariablesManager";
|
||||||
import { playersStore } from "../../Stores/PlayersStore";
|
import { playersStore } from "../../Stores/PlayersStore";
|
||||||
import { chatVisibilityStore } from "../../Stores/ChatStore";
|
import { chatVisibilityStore } from "../../Stores/ChatStore";
|
||||||
|
|
||||||
@ -202,7 +197,8 @@ export class GameScene extends DirtyScene {
|
|||||||
private mapTransitioning: boolean = false; //used to prevent transitions happenning at the same time.
|
private mapTransitioning: boolean = false; //used to prevent transitions happenning at the same time.
|
||||||
private emoteManager!: EmoteManager;
|
private emoteManager!: EmoteManager;
|
||||||
private preloading: boolean = true;
|
private preloading: boolean = true;
|
||||||
startPositionCalculator!: StartPositionCalculator;
|
private startPositionCalculator!: StartPositionCalculator;
|
||||||
|
private sharedVariablesManager!: SharedVariablesManager;
|
||||||
|
|
||||||
constructor(private room: Room, MapUrlFile: string, customKey?: string | undefined) {
|
constructor(private room: Room, MapUrlFile: string, customKey?: string | undefined) {
|
||||||
super({
|
super({
|
||||||
@ -718,6 +714,13 @@ export class GameScene extends DirtyScene {
|
|||||||
this.gameMap.setPosition(event.x, event.y);
|
this.gameMap.setPosition(event.x, event.y);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Set up variables manager
|
||||||
|
this.sharedVariablesManager = new SharedVariablesManager(
|
||||||
|
this.connection,
|
||||||
|
this.gameMap,
|
||||||
|
onConnect.room.variables
|
||||||
|
);
|
||||||
|
|
||||||
//this.initUsersPosition(roomJoinedMessage.users);
|
//this.initUsersPosition(roomJoinedMessage.users);
|
||||||
this.connectionAnswerPromiseResolve(onConnect.room);
|
this.connectionAnswerPromiseResolve(onConnect.room);
|
||||||
// Analyze tags to find if we are admin. If yes, show console.
|
// Analyze tags to find if we are admin. If yes, show console.
|
||||||
@ -1053,20 +1056,24 @@ ${escapedMessage}
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
this.iframeSubscriptionList.push(
|
iframeListener.registerAnswerer("getMapData", () => {
|
||||||
iframeListener.dataLayerChangeStream.subscribe(() => {
|
return {
|
||||||
iframeListener.sendDataLayerEvent({ data: this.gameMap.getMap() });
|
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 {
|
return {
|
||||||
mapUrl: this.MapUrlFile,
|
mapUrl: this.MapUrlFile,
|
||||||
startLayerName: this.startPositionCalculator.startLayerName,
|
startLayerName: this.startPositionCalculator.startLayerName,
|
||||||
uuid: localUserStore.getLocalUser()?.uuid,
|
uuid: localUserStore.getLocalUser()?.uuid,
|
||||||
nickname: localUserStore.getName(),
|
nickname: this.playerName,
|
||||||
roomId: this.roomUrl,
|
roomId: this.roomUrl,
|
||||||
tags: this.connection ? this.connection.getAllTags() : [],
|
tags: this.connection ? this.connection.getAllTags() : [],
|
||||||
|
variables: this.sharedVariablesManager.variables,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
this.iframeSubscriptionList.push(
|
this.iframeSubscriptionList.push(
|
||||||
@ -1197,6 +1204,7 @@ ${escapedMessage}
|
|||||||
this.chatVisibilityUnsubscribe();
|
this.chatVisibilityUnsubscribe();
|
||||||
this.biggestAvailableAreaStoreUnsubscribe();
|
this.biggestAvailableAreaStoreUnsubscribe();
|
||||||
iframeListener.unregisterAnswerer("getState");
|
iframeListener.unregisterAnswerer("getState");
|
||||||
|
this.sharedVariablesManager?.close();
|
||||||
|
|
||||||
mediaManager.hideGameOverlay();
|
mediaManager.hideGameOverlay();
|
||||||
|
|
||||||
@ -1236,12 +1244,12 @@ ${escapedMessage}
|
|||||||
}
|
}
|
||||||
|
|
||||||
private getProperty(layer: ITiledMapLayer | ITiledMap, name: string): string | boolean | number | undefined {
|
private getProperty(layer: ITiledMapLayer | ITiledMap, name: string): string | boolean | number | undefined {
|
||||||
const properties: ITiledMapLayerProperty[] | undefined = layer.properties;
|
const properties: ITiledMapProperty[] | undefined = layer.properties;
|
||||||
if (!properties) {
|
if (!properties) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
const obj = properties.find(
|
const obj = properties.find(
|
||||||
(property: ITiledMapLayerProperty) => property.name.toLowerCase() === name.toLowerCase()
|
(property: ITiledMapProperty) => property.name.toLowerCase() === name.toLowerCase()
|
||||||
);
|
);
|
||||||
if (obj === undefined) {
|
if (obj === undefined) {
|
||||||
return undefined;
|
return undefined;
|
||||||
@ -1250,12 +1258,12 @@ ${escapedMessage}
|
|||||||
}
|
}
|
||||||
|
|
||||||
private getProperties(layer: ITiledMapLayer | ITiledMap, name: string): (string | number | boolean | undefined)[] {
|
private getProperties(layer: ITiledMapLayer | ITiledMap, name: string): (string | number | boolean | undefined)[] {
|
||||||
const properties: ITiledMapLayerProperty[] | undefined = layer.properties;
|
const properties: ITiledMapProperty[] | undefined = layer.properties;
|
||||||
if (!properties) {
|
if (!properties) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
return properties
|
return properties
|
||||||
.filter((property: ITiledMapLayerProperty) => property.name.toLowerCase() === name.toLowerCase())
|
.filter((property: ITiledMapProperty) => property.name.toLowerCase() === name.toLowerCase())
|
||||||
.map((property) => property.value);
|
.map((property) => property.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
159
front/src/Phaser/Game/SharedVariablesManager.ts
Normal file
159
front/src/Phaser/Game/SharedVariablesManager.ts
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
import type { RoomConnection } from "../../Connexion/RoomConnection";
|
||||||
|
import { iframeListener } from "../../Api/IframeListener";
|
||||||
|
import type { Subscription } from "rxjs";
|
||||||
|
import type { GameMap } from "./GameMap";
|
||||||
|
import type { ITile, ITiledMapObject } from "../Map/ITiledMap";
|
||||||
|
import type { Var } from "svelte/types/compiler/interfaces";
|
||||||
|
import { init } from "svelte/internal";
|
||||||
|
|
||||||
|
interface Variable {
|
||||||
|
defaultValue: unknown;
|
||||||
|
readableBy?: string;
|
||||||
|
writableBy?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores variables and provides a bridge between scripts and the pusher server.
|
||||||
|
*/
|
||||||
|
export class SharedVariablesManager {
|
||||||
|
private _variables = new Map<string, unknown>();
|
||||||
|
private variableObjects: Map<string, Variable>;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private roomConnection: RoomConnection,
|
||||||
|
private gameMap: GameMap,
|
||||||
|
serverVariables: Map<string, unknown>
|
||||||
|
) {
|
||||||
|
// We initialize the list of variable object at room start. The objects cannot be edited later
|
||||||
|
// (otherwise, this would cause a security issue if the scripting API can edit this list of objects)
|
||||||
|
this.variableObjects = SharedVariablesManager.findVariablesInMap(gameMap);
|
||||||
|
|
||||||
|
// Let's initialize default values
|
||||||
|
for (const [name, variableObject] of this.variableObjects.entries()) {
|
||||||
|
if (variableObject.readableBy && !this.roomConnection.hasTag(variableObject.readableBy)) {
|
||||||
|
// Do not initialize default value for variables that are not readable
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._variables.set(name, variableObject.defaultValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Override default values with the variables from the server:
|
||||||
|
for (const [name, value] of serverVariables) {
|
||||||
|
this._variables.set(name, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
roomConnection.onSetVariable((name, value) => {
|
||||||
|
this._variables.set(name, value);
|
||||||
|
|
||||||
|
// On server change, let's notify the iframes
|
||||||
|
iframeListener.setVariable({
|
||||||
|
key: name,
|
||||||
|
value: value,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// When a variable is modified from an iFrame
|
||||||
|
iframeListener.registerAnswerer("setVariable", (event) => {
|
||||||
|
const key = event.key;
|
||||||
|
|
||||||
|
const object = this.variableObjects.get(key);
|
||||||
|
|
||||||
|
if (object === undefined) {
|
||||||
|
const errMsg =
|
||||||
|
'A script is trying to modify variable "' +
|
||||||
|
key +
|
||||||
|
'" but this variable is not defined in the map.' +
|
||||||
|
'There should be an object in the map whose name is "' +
|
||||||
|
key +
|
||||||
|
'" and whose type is "variable"';
|
||||||
|
console.error(errMsg);
|
||||||
|
throw new Error(errMsg);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (object.writableBy && !this.roomConnection.hasTag(object.writableBy)) {
|
||||||
|
const errMsg =
|
||||||
|
'A script is trying to modify variable "' +
|
||||||
|
key +
|
||||||
|
'" but this variable is only writable for users with tag "' +
|
||||||
|
object.writableBy +
|
||||||
|
'".';
|
||||||
|
console.error(errMsg);
|
||||||
|
throw new Error(errMsg);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._variables.set(key, event.value);
|
||||||
|
|
||||||
|
// Dispatch to the room connection.
|
||||||
|
this.roomConnection.emitSetVariableEvent(key, event.value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static findVariablesInMap(gameMap: GameMap): Map<string, Variable> {
|
||||||
|
const objects = new Map<string, Variable>();
|
||||||
|
for (const layer of gameMap.getMap().layers) {
|
||||||
|
if (layer.type === "objectgroup") {
|
||||||
|
for (const object of layer.objects) {
|
||||||
|
if (object.type === "variable") {
|
||||||
|
if (object.template) {
|
||||||
|
console.warn(
|
||||||
|
'Warning, a variable object is using a Tiled "template". WorkAdventure does not support objects generated from Tiled templates.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// We store a copy of the object (to make it immutable)
|
||||||
|
objects.set(object.name, this.iTiledObjectToVariable(object));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return objects;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static iTiledObjectToVariable(object: ITiledMapObject): Variable {
|
||||||
|
const variable: Variable = {
|
||||||
|
defaultValue: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (object.properties) {
|
||||||
|
for (const property of object.properties) {
|
||||||
|
const value = property.value;
|
||||||
|
switch (property.name) {
|
||||||
|
case "default":
|
||||||
|
variable.defaultValue = value;
|
||||||
|
break;
|
||||||
|
case "writableBy":
|
||||||
|
if (typeof value !== "string") {
|
||||||
|
throw new Error(
|
||||||
|
'The writableBy property of variable "' + object.name + '" must be a string'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (value) {
|
||||||
|
variable.writableBy = value;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "readableBy":
|
||||||
|
if (typeof value !== "string") {
|
||||||
|
throw new Error(
|
||||||
|
'The readableBy property of variable "' + object.name + '" must be a string'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (value) {
|
||||||
|
variable.readableBy = value;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return variable;
|
||||||
|
}
|
||||||
|
|
||||||
|
public close(): void {
|
||||||
|
iframeListener.unregisterAnswerer("setVariable");
|
||||||
|
}
|
||||||
|
|
||||||
|
get variables(): Map<string, unknown> {
|
||||||
|
return this._variables;
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
import type { PositionInterface } from "../../Connexion/ConnexionModels";
|
import type { PositionInterface } from "../../Connexion/ConnexionModels";
|
||||||
import type { ITiledMap, ITiledMapLayer, ITiledMapLayerProperty, ITiledMapTileLayer } from "../Map/ITiledMap";
|
import type { ITiledMap, ITiledMapLayer, ITiledMapProperty, ITiledMapTileLayer } from "../Map/ITiledMap";
|
||||||
import type { GameMap } from "./GameMap";
|
import type { GameMap } from "./GameMap";
|
||||||
|
|
||||||
const defaultStartLayerName = "start";
|
const defaultStartLayerName = "start";
|
||||||
@ -112,12 +112,12 @@ export class StartPositionCalculator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private getProperty(layer: ITiledMapLayer | ITiledMap, name: string): string | boolean | number | undefined {
|
private getProperty(layer: ITiledMapLayer | ITiledMap, name: string): string | boolean | number | undefined {
|
||||||
const properties: ITiledMapLayerProperty[] | undefined = layer.properties;
|
const properties: ITiledMapProperty[] | undefined = layer.properties;
|
||||||
if (!properties) {
|
if (!properties) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
const obj = properties.find(
|
const obj = properties.find(
|
||||||
(property: ITiledMapLayerProperty) => property.name.toLowerCase() === name.toLowerCase()
|
(property: ITiledMapProperty) => property.name.toLowerCase() === name.toLowerCase()
|
||||||
);
|
);
|
||||||
if (obj === undefined) {
|
if (obj === undefined) {
|
||||||
return undefined;
|
return undefined;
|
||||||
|
@ -16,7 +16,7 @@ export interface ITiledMap {
|
|||||||
* Map orientation (orthogonal)
|
* Map orientation (orthogonal)
|
||||||
*/
|
*/
|
||||||
orientation: string;
|
orientation: string;
|
||||||
properties?: ITiledMapLayerProperty[];
|
properties?: ITiledMapProperty[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render order (right-down)
|
* Render order (right-down)
|
||||||
@ -33,7 +33,7 @@ export interface ITiledMap {
|
|||||||
type?: string;
|
type?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ITiledMapLayerProperty {
|
export interface ITiledMapProperty {
|
||||||
name: string;
|
name: string;
|
||||||
type: string;
|
type: string;
|
||||||
value: string | boolean | number | undefined;
|
value: string | boolean | number | undefined;
|
||||||
@ -51,7 +51,7 @@ export interface ITiledMapGroupLayer {
|
|||||||
id?: number;
|
id?: number;
|
||||||
name: string;
|
name: string;
|
||||||
opacity: number;
|
opacity: number;
|
||||||
properties?: ITiledMapLayerProperty[];
|
properties?: ITiledMapProperty[];
|
||||||
|
|
||||||
type: "group";
|
type: "group";
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
@ -69,7 +69,7 @@ export interface ITiledMapTileLayer {
|
|||||||
height: number;
|
height: number;
|
||||||
name: string;
|
name: string;
|
||||||
opacity: number;
|
opacity: number;
|
||||||
properties?: ITiledMapLayerProperty[];
|
properties?: ITiledMapProperty[];
|
||||||
encoding?: string;
|
encoding?: string;
|
||||||
compression?: string;
|
compression?: string;
|
||||||
|
|
||||||
@ -91,7 +91,7 @@ export interface ITiledMapObjectLayer {
|
|||||||
height: number;
|
height: number;
|
||||||
name: string;
|
name: string;
|
||||||
opacity: number;
|
opacity: number;
|
||||||
properties?: ITiledMapLayerProperty[];
|
properties?: ITiledMapProperty[];
|
||||||
encoding?: string;
|
encoding?: string;
|
||||||
compression?: string;
|
compression?: string;
|
||||||
|
|
||||||
@ -117,7 +117,7 @@ export interface ITiledMapObject {
|
|||||||
gid: number;
|
gid: number;
|
||||||
height: number;
|
height: number;
|
||||||
name: string;
|
name: string;
|
||||||
properties: { [key: string]: string };
|
properties?: ITiledMapProperty[];
|
||||||
rotation: number;
|
rotation: number;
|
||||||
type: string;
|
type: string;
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
@ -141,6 +141,7 @@ export interface ITiledMapObject {
|
|||||||
polyline: { x: number; y: number }[];
|
polyline: { x: number; y: number }[];
|
||||||
|
|
||||||
text?: ITiledText;
|
text?: ITiledText;
|
||||||
|
template?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ITiledText {
|
export interface ITiledText {
|
||||||
@ -163,7 +164,7 @@ export interface ITiledTileSet {
|
|||||||
imagewidth: number;
|
imagewidth: number;
|
||||||
margin: number;
|
margin: number;
|
||||||
name: string;
|
name: string;
|
||||||
properties: { [key: string]: string };
|
properties?: ITiledMapProperty[];
|
||||||
spacing: number;
|
spacing: number;
|
||||||
tilecount: number;
|
tilecount: number;
|
||||||
tileheight: number;
|
tileheight: number;
|
||||||
@ -182,7 +183,7 @@ export interface ITile {
|
|||||||
id: number;
|
id: number;
|
||||||
type?: string;
|
type?: string;
|
||||||
|
|
||||||
properties?: Array<ITiledMapLayerProperty>;
|
properties?: ITiledMapProperty[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ITiledMapTerrain {
|
export interface ITiledMapTerrain {
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
import {TextField} from "../Components/TextField";
|
import { TextField } from "../Components/TextField";
|
||||||
import Image = Phaser.GameObjects.Image;
|
import Image = Phaser.GameObjects.Image;
|
||||||
import Sprite = Phaser.GameObjects.Sprite;
|
import Sprite = Phaser.GameObjects.Sprite;
|
||||||
import Text = Phaser.GameObjects.Text;
|
import Text = Phaser.GameObjects.Text;
|
||||||
import ScenePlugin = Phaser.Scenes.ScenePlugin;
|
import ScenePlugin = Phaser.Scenes.ScenePlugin;
|
||||||
import {WAError} from "./WAError";
|
import { WAError } from "./WAError";
|
||||||
|
|
||||||
export const ErrorSceneName = "ErrorScene";
|
export const ErrorSceneName = "ErrorScene";
|
||||||
enum Textures {
|
enum Textures {
|
||||||
icon = "icon",
|
icon = "icon",
|
||||||
mainFont = "main_font"
|
mainFont = "main_font",
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ErrorScene extends Phaser.Scene {
|
export class ErrorScene extends Phaser.Scene {
|
||||||
@ -23,25 +23,21 @@ export class ErrorScene extends Phaser.Scene {
|
|||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super({
|
super({
|
||||||
key: ErrorSceneName
|
key: ErrorSceneName,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
init({title, subTitle, message}: { title?: string, subTitle?: string, message?: string }) {
|
init({ title, subTitle, message }: { title?: string; subTitle?: string; message?: string }) {
|
||||||
this.title = title ? title : '';
|
this.title = title ? title : "";
|
||||||
this.subTitle = subTitle ? subTitle : '';
|
this.subTitle = subTitle ? subTitle : "";
|
||||||
this.message = message ? message : '';
|
this.message = message ? message : "";
|
||||||
}
|
}
|
||||||
|
|
||||||
preload() {
|
preload() {
|
||||||
this.load.image(Textures.icon, "static/images/favicons/favicon-32x32.png");
|
this.load.image(Textures.icon, "static/images/favicons/favicon-32x32.png");
|
||||||
// Note: arcade.png from the Phaser 3 examples at: https://github.com/photonstorm/phaser3-examples/tree/master/public/assets/fonts/bitmap
|
// Note: arcade.png from the Phaser 3 examples at: https://github.com/photonstorm/phaser3-examples/tree/master/public/assets/fonts/bitmap
|
||||||
this.load.bitmapFont(Textures.mainFont, 'resources/fonts/arcade.png', 'resources/fonts/arcade.xml');
|
this.load.bitmapFont(Textures.mainFont, "resources/fonts/arcade.png", "resources/fonts/arcade.xml");
|
||||||
this.load.spritesheet(
|
this.load.spritesheet("cat", "resources/characters/pipoya/Cat 01-1.png", { frameWidth: 32, frameHeight: 32 });
|
||||||
'cat',
|
|
||||||
'resources/characters/pipoya/Cat 01-1.png',
|
|
||||||
{frameWidth: 32, frameHeight: 32}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
create() {
|
create() {
|
||||||
@ -50,15 +46,25 @@ export class ErrorScene extends Phaser.Scene {
|
|||||||
|
|
||||||
this.titleField = new TextField(this, this.game.renderer.width / 2, this.game.renderer.height / 2, this.title);
|
this.titleField = new TextField(this, this.game.renderer.width / 2, this.game.renderer.height / 2, this.title);
|
||||||
|
|
||||||
this.subTitleField = new TextField(this, this.game.renderer.width / 2, this.game.renderer.height / 2 + 24, this.subTitle);
|
this.subTitleField = new TextField(
|
||||||
|
this,
|
||||||
|
this.game.renderer.width / 2,
|
||||||
|
this.game.renderer.height / 2 + 24,
|
||||||
|
this.subTitle
|
||||||
|
);
|
||||||
|
|
||||||
this.messageField = this.add.text(this.game.renderer.width / 2, this.game.renderer.height / 2 + 48, this.message, {
|
this.messageField = this.add.text(
|
||||||
|
this.game.renderer.width / 2,
|
||||||
|
this.game.renderer.height / 2 + 48,
|
||||||
|
this.message,
|
||||||
|
{
|
||||||
fontFamily: 'Georgia, "Goudy Bookletter 1911", Times, serif',
|
fontFamily: 'Georgia, "Goudy Bookletter 1911", Times, serif',
|
||||||
fontSize: '10px'
|
fontSize: "10px",
|
||||||
});
|
}
|
||||||
|
);
|
||||||
this.messageField.setOrigin(0.5, 0.5);
|
this.messageField.setOrigin(0.5, 0.5);
|
||||||
|
|
||||||
this.cat = this.physics.add.sprite(this.game.renderer.width / 2, this.game.renderer.height / 2 - 32, 'cat', 6);
|
this.cat = this.physics.add.sprite(this.game.renderer.width / 2, this.game.renderer.height / 2 - 32, "cat", 6);
|
||||||
this.cat.flipY = true;
|
this.cat.flipY = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -69,38 +75,38 @@ export class ErrorScene extends Phaser.Scene {
|
|||||||
public static showError(error: any, scene: ScenePlugin): void {
|
public static showError(error: any, scene: ScenePlugin): void {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
|
||||||
if (typeof error === 'string' || error instanceof String) {
|
if (typeof error === "string" || error instanceof String) {
|
||||||
scene.start(ErrorSceneName, {
|
scene.start(ErrorSceneName, {
|
||||||
title: 'An error occurred',
|
title: "An error occurred",
|
||||||
subTitle: error
|
subTitle: error,
|
||||||
});
|
});
|
||||||
} else if (error instanceof WAError) {
|
} else if (error instanceof WAError) {
|
||||||
scene.start(ErrorSceneName, {
|
scene.start(ErrorSceneName, {
|
||||||
title: error.title,
|
title: error.title,
|
||||||
subTitle: error.subTitle,
|
subTitle: error.subTitle,
|
||||||
message: error.details
|
message: error.details,
|
||||||
});
|
});
|
||||||
} else if (error.response) {
|
} else if (error.response) {
|
||||||
// Axios HTTP error
|
// Axios HTTP error
|
||||||
// client received an error response (5xx, 4xx)
|
// client received an error response (5xx, 4xx)
|
||||||
scene.start(ErrorSceneName, {
|
scene.start(ErrorSceneName, {
|
||||||
title: 'HTTP ' + error.response.status + ' - ' + error.response.statusText,
|
title: "HTTP " + error.response.status + " - " + error.response.statusText,
|
||||||
subTitle: 'An error occurred while accessing URL:',
|
subTitle: "An error occurred while accessing URL:",
|
||||||
message: error.response.config.url
|
message: error.response.config.url,
|
||||||
});
|
});
|
||||||
} else if (error.request) {
|
} else if (error.request) {
|
||||||
// Axios HTTP error
|
// Axios HTTP error
|
||||||
// client never received a response, or request never left
|
// client never received a response, or request never left
|
||||||
scene.start(ErrorSceneName, {
|
scene.start(ErrorSceneName, {
|
||||||
title: 'Network error',
|
title: "Network error",
|
||||||
subTitle: error.message
|
subTitle: error.message,
|
||||||
});
|
});
|
||||||
} else if (error instanceof Error) {
|
} else if (error instanceof Error) {
|
||||||
// Error
|
// Error
|
||||||
scene.start(ErrorSceneName, {
|
scene.start(ErrorSceneName, {
|
||||||
title: 'An error occurred',
|
title: "An error occurred",
|
||||||
subTitle: error.name,
|
subTitle: error.name,
|
||||||
message: error.message
|
message: error.message,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
throw error;
|
throw error;
|
||||||
@ -114,7 +120,7 @@ export class ErrorScene extends Phaser.Scene {
|
|||||||
scene.start(ErrorSceneName, {
|
scene.start(ErrorSceneName, {
|
||||||
title,
|
title,
|
||||||
subTitle,
|
subTitle,
|
||||||
message
|
message,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import {TextField} from "../Components/TextField";
|
import { TextField } from "../Components/TextField";
|
||||||
import Image = Phaser.GameObjects.Image;
|
import Image = Phaser.GameObjects.Image;
|
||||||
import Sprite = Phaser.GameObjects.Sprite;
|
import Sprite = Phaser.GameObjects.Sprite;
|
||||||
|
|
||||||
export const ReconnectingSceneName = "ReconnectingScene";
|
export const ReconnectingSceneName = "ReconnectingScene";
|
||||||
enum ReconnectingTextures {
|
enum ReconnectingTextures {
|
||||||
icon = "icon",
|
icon = "icon",
|
||||||
mainFont = "main_font"
|
mainFont = "main_font",
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ReconnectingScene extends Phaser.Scene {
|
export class ReconnectingScene extends Phaser.Scene {
|
||||||
@ -14,35 +14,40 @@ export class ReconnectingScene extends Phaser.Scene {
|
|||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super({
|
super({
|
||||||
key: ReconnectingSceneName
|
key: ReconnectingSceneName,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
preload() {
|
preload() {
|
||||||
this.load.image(ReconnectingTextures.icon, "static/images/favicons/favicon-32x32.png");
|
this.load.image(ReconnectingTextures.icon, "static/images/favicons/favicon-32x32.png");
|
||||||
// Note: arcade.png from the Phaser 3 examples at: https://github.com/photonstorm/phaser3-examples/tree/master/public/assets/fonts/bitmap
|
// Note: arcade.png from the Phaser 3 examples at: https://github.com/photonstorm/phaser3-examples/tree/master/public/assets/fonts/bitmap
|
||||||
this.load.bitmapFont(ReconnectingTextures.mainFont, 'resources/fonts/arcade.png', 'resources/fonts/arcade.xml');
|
this.load.bitmapFont(ReconnectingTextures.mainFont, "resources/fonts/arcade.png", "resources/fonts/arcade.xml");
|
||||||
this.load.spritesheet(
|
this.load.spritesheet("cat", "resources/characters/pipoya/Cat 01-1.png", { frameWidth: 32, frameHeight: 32 });
|
||||||
'cat',
|
|
||||||
'resources/characters/pipoya/Cat 01-1.png',
|
|
||||||
{frameWidth: 32, frameHeight: 32}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
create() {
|
create() {
|
||||||
this.logo = new Image(this, this.game.renderer.width - 30, this.game.renderer.height - 20, ReconnectingTextures.icon);
|
this.logo = new Image(
|
||||||
|
this,
|
||||||
|
this.game.renderer.width - 30,
|
||||||
|
this.game.renderer.height - 20,
|
||||||
|
ReconnectingTextures.icon
|
||||||
|
);
|
||||||
this.add.existing(this.logo);
|
this.add.existing(this.logo);
|
||||||
|
|
||||||
this.reconnectingField = new TextField(this, this.game.renderer.width / 2, this.game.renderer.height / 2, "Connection lost. Reconnecting...");
|
this.reconnectingField = new TextField(
|
||||||
|
this,
|
||||||
|
this.game.renderer.width / 2,
|
||||||
|
this.game.renderer.height / 2,
|
||||||
|
"Connection lost. Reconnecting..."
|
||||||
|
);
|
||||||
|
|
||||||
const cat = this.physics.add.sprite(this.game.renderer.width / 2, this.game.renderer.height / 2 - 32, 'cat');
|
const cat = this.physics.add.sprite(this.game.renderer.width / 2, this.game.renderer.height / 2 - 32, "cat");
|
||||||
this.anims.create({
|
this.anims.create({
|
||||||
key: 'right',
|
key: "right",
|
||||||
frames: this.anims.generateFrameNumbers('cat', { start: 6, end: 8 }),
|
frames: this.anims.generateFrameNumbers("cat", { start: 6, end: 8 }),
|
||||||
frameRate: 10,
|
frameRate: 10,
|
||||||
repeat: -1
|
repeat: -1,
|
||||||
});
|
});
|
||||||
cat.play('right');
|
cat.play("right");
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@ export class HtmlUtils {
|
|||||||
if (HtmlUtils.isHtmlElement<T>(elem)) {
|
if (HtmlUtils.isHtmlElement<T>(elem)) {
|
||||||
return elem;
|
return elem;
|
||||||
}
|
}
|
||||||
throw new Error("Cannot find HTML element with id '"+id+"'");
|
throw new Error("Cannot find HTML element with id '" + id + "'");
|
||||||
}
|
}
|
||||||
|
|
||||||
public static querySelectorOrFail<T extends HTMLElement>(selector: string): T {
|
public static querySelectorOrFail<T extends HTMLElement>(selector: string): T {
|
||||||
@ -12,7 +12,7 @@ export class HtmlUtils {
|
|||||||
if (HtmlUtils.isHtmlElement<T>(elem)) {
|
if (HtmlUtils.isHtmlElement<T>(elem)) {
|
||||||
return elem;
|
return elem;
|
||||||
}
|
}
|
||||||
throw new Error("Cannot find HTML element with selector '"+selector+"'");
|
throw new Error("Cannot find HTML element with selector '" + selector + "'");
|
||||||
}
|
}
|
||||||
|
|
||||||
public static removeElementByIdOrFail<T extends HTMLElement>(id: string): T {
|
public static removeElementByIdOrFail<T extends HTMLElement>(id: string): T {
|
||||||
@ -21,12 +21,12 @@ export class HtmlUtils {
|
|||||||
elem.remove();
|
elem.remove();
|
||||||
return elem;
|
return elem;
|
||||||
}
|
}
|
||||||
throw new Error("Cannot find HTML element with id '"+id+"'");
|
throw new Error("Cannot find HTML element with id '" + id + "'");
|
||||||
}
|
}
|
||||||
|
|
||||||
public static escapeHtml(html: string): string {
|
public static escapeHtml(html: string): string {
|
||||||
const text = document.createTextNode(html.replace(/(\r\n|\r|\n)/g,'<br/>'));
|
const text = document.createTextNode(html.replace(/(\r\n|\r|\n)/g, "<br/>"));
|
||||||
const p = document.createElement('p');
|
const p = document.createElement("p");
|
||||||
p.appendChild(text);
|
p.appendChild(text);
|
||||||
return p.innerHTML;
|
return p.innerHTML;
|
||||||
}
|
}
|
||||||
@ -35,7 +35,7 @@ export class HtmlUtils {
|
|||||||
const urlRegex = /(https?:\/\/[^\s]+)/g;
|
const urlRegex = /(https?:\/\/[^\s]+)/g;
|
||||||
text = HtmlUtils.escapeHtml(text);
|
text = HtmlUtils.escapeHtml(text);
|
||||||
return text.replace(urlRegex, (url: string) => {
|
return text.replace(urlRegex, (url: string) => {
|
||||||
const link = document.createElement('a');
|
const link = document.createElement("a");
|
||||||
link.href = url;
|
link.href = url;
|
||||||
link.target = "_blank";
|
link.target = "_blank";
|
||||||
const text = document.createTextNode(url);
|
const text = document.createTextNode(url);
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
import { registeredCallbacks } from "./Api/iframe/registeredCallbacks";
|
import { registeredCallbacks } from "./Api/iframe/registeredCallbacks";
|
||||||
import {
|
import {
|
||||||
IframeResponseEvent,
|
IframeResponseEvent,
|
||||||
IframeResponseEventMap, isIframeAnswerEvent, isIframeErrorAnswerEvent,
|
IframeResponseEventMap,
|
||||||
|
isIframeAnswerEvent,
|
||||||
|
isIframeErrorAnswerEvent,
|
||||||
isIframeResponseEventWrapper,
|
isIframeResponseEventWrapper,
|
||||||
TypedMessageEvent,
|
TypedMessageEvent,
|
||||||
} from "./Api/Events/IframeEvent";
|
} from "./Api/Events/IframeEvent";
|
||||||
@ -11,12 +13,26 @@ import nav from "./Api/iframe/nav";
|
|||||||
import controls from "./Api/iframe/controls";
|
import controls from "./Api/iframe/controls";
|
||||||
import ui from "./Api/iframe/ui";
|
import ui from "./Api/iframe/ui";
|
||||||
import sound from "./Api/iframe/sound";
|
import sound from "./Api/iframe/sound";
|
||||||
import room from "./Api/iframe/room";
|
import room, { setMapURL, setRoomId } from "./Api/iframe/room";
|
||||||
import player from "./Api/iframe/player";
|
import state, { initVariables } from "./Api/iframe/state";
|
||||||
|
import player, { setPlayerName, setTags, setUuid } from "./Api/iframe/player";
|
||||||
import type { ButtonDescriptor } from "./Api/iframe/Ui/ButtonDescriptor";
|
import type { ButtonDescriptor } from "./Api/iframe/Ui/ButtonDescriptor";
|
||||||
import type { Popup } from "./Api/iframe/Ui/Popup";
|
import type { Popup } from "./Api/iframe/Ui/Popup";
|
||||||
import type { Sound } from "./Api/iframe/Sound/Sound";
|
import type { Sound } from "./Api/iframe/Sound/Sound";
|
||||||
import { answerPromises, sendToWorkadventure} from "./Api/iframe/IframeApiContribution";
|
import { answerPromises, queryWorkadventure, sendToWorkadventure } from "./Api/iframe/IframeApiContribution";
|
||||||
|
|
||||||
|
// Notify WorkAdventure that we are ready to receive data
|
||||||
|
const initPromise = queryWorkadventure({
|
||||||
|
type: "getState",
|
||||||
|
data: undefined,
|
||||||
|
}).then((state) => {
|
||||||
|
setPlayerName(state.nickname);
|
||||||
|
setRoomId(state.roomId);
|
||||||
|
setMapURL(state.mapUrl);
|
||||||
|
setTags(state.tags);
|
||||||
|
setUuid(state.uuid);
|
||||||
|
initVariables(state.variables as Map<string, unknown>);
|
||||||
|
});
|
||||||
|
|
||||||
const wa = {
|
const wa = {
|
||||||
ui,
|
ui,
|
||||||
@ -26,6 +42,11 @@ const wa = {
|
|||||||
sound,
|
sound,
|
||||||
room,
|
room,
|
||||||
player,
|
player,
|
||||||
|
state,
|
||||||
|
|
||||||
|
onInit(): Promise<void> {
|
||||||
|
return initPromise;
|
||||||
|
},
|
||||||
|
|
||||||
// All methods below are deprecated and should not be used anymore.
|
// All methods below are deprecated and should not be used anymore.
|
||||||
// They are kept here for backward compatibility.
|
// They are kept here for backward compatibility.
|
||||||
@ -164,34 +185,35 @@ declare global {
|
|||||||
window.WA = wa;
|
window.WA = wa;
|
||||||
|
|
||||||
window.addEventListener(
|
window.addEventListener(
|
||||||
"message", <T extends keyof IframeResponseEventMap>(message: TypedMessageEvent<IframeResponseEvent<T>>) => {
|
"message",
|
||||||
|
<T extends keyof IframeResponseEventMap>(message: TypedMessageEvent<IframeResponseEvent<T>>) => {
|
||||||
if (message.source !== window.parent) {
|
if (message.source !== window.parent) {
|
||||||
return; // Skip message in this event listener
|
return; // Skip message in this event listener
|
||||||
}
|
}
|
||||||
const payload = message.data;
|
const payload = message.data;
|
||||||
|
|
||||||
console.debug(payload);
|
//console.debug(payload);
|
||||||
|
|
||||||
if (isIframeAnswerEvent(payload)) {
|
if (isIframeErrorAnswerEvent(payload)) {
|
||||||
const queryId = payload.id;
|
|
||||||
const payloadData = payload.data;
|
|
||||||
|
|
||||||
const resolver = answerPromises.get(queryId);
|
|
||||||
if (resolver === undefined) {
|
|
||||||
throw new Error('In Iframe API, got an answer for a question that we have no track of.');
|
|
||||||
}
|
|
||||||
resolver.resolve(payloadData);
|
|
||||||
|
|
||||||
answerPromises.delete(queryId);
|
|
||||||
} else if (isIframeErrorAnswerEvent(payload)) {
|
|
||||||
const queryId = payload.id;
|
const queryId = payload.id;
|
||||||
const payloadError = payload.error;
|
const payloadError = payload.error;
|
||||||
|
|
||||||
const resolver = answerPromises.get(queryId);
|
const resolver = answerPromises.get(queryId);
|
||||||
if (resolver === undefined) {
|
if (resolver === undefined) {
|
||||||
throw new Error('In Iframe API, got an error answer for a question that we have no track of.');
|
throw new Error("In Iframe API, got an error answer for a question that we have no track of.");
|
||||||
}
|
}
|
||||||
resolver.reject(payloadError);
|
resolver.reject(new Error(payloadError));
|
||||||
|
|
||||||
|
answerPromises.delete(queryId);
|
||||||
|
} else if (isIframeAnswerEvent(payload)) {
|
||||||
|
const queryId = payload.id;
|
||||||
|
const payloadData = payload.data;
|
||||||
|
|
||||||
|
const resolver = answerPromises.get(queryId);
|
||||||
|
if (resolver === undefined) {
|
||||||
|
throw new Error("In Iframe API, got an answer for a question that we have no track of.");
|
||||||
|
}
|
||||||
|
resolver.resolve(payloadData);
|
||||||
|
|
||||||
answerPromises.delete(queryId);
|
answerPromises.delete(queryId);
|
||||||
} else if (isIframeResponseEventWrapper(payload)) {
|
} else if (isIframeResponseEventWrapper(payload)) {
|
||||||
|
@ -1,35 +1,34 @@
|
|||||||
import 'phaser';
|
import "phaser";
|
||||||
import GameConfig = Phaser.Types.Core.GameConfig;
|
import GameConfig = Phaser.Types.Core.GameConfig;
|
||||||
import "../style/index.scss";
|
import "../style/index.scss";
|
||||||
|
|
||||||
import {DEBUG_MODE, isMobile} from "./Enum/EnvironmentVariable";
|
import { DEBUG_MODE, isMobile } from "./Enum/EnvironmentVariable";
|
||||||
import {LoginScene} from "./Phaser/Login/LoginScene";
|
import { LoginScene } from "./Phaser/Login/LoginScene";
|
||||||
import {ReconnectingScene} from "./Phaser/Reconnecting/ReconnectingScene";
|
import { ReconnectingScene } from "./Phaser/Reconnecting/ReconnectingScene";
|
||||||
import {SelectCharacterScene} from "./Phaser/Login/SelectCharacterScene";
|
import { SelectCharacterScene } from "./Phaser/Login/SelectCharacterScene";
|
||||||
import {SelectCompanionScene} from "./Phaser/Login/SelectCompanionScene";
|
import { SelectCompanionScene } from "./Phaser/Login/SelectCompanionScene";
|
||||||
import {EnableCameraScene} from "./Phaser/Login/EnableCameraScene";
|
import { EnableCameraScene } from "./Phaser/Login/EnableCameraScene";
|
||||||
import {CustomizeScene} from "./Phaser/Login/CustomizeScene";
|
import { CustomizeScene } from "./Phaser/Login/CustomizeScene";
|
||||||
import WebFontLoaderPlugin from 'phaser3-rex-plugins/plugins/webfontloader-plugin.js';
|
import WebFontLoaderPlugin from "phaser3-rex-plugins/plugins/webfontloader-plugin.js";
|
||||||
import OutlinePipelinePlugin from 'phaser3-rex-plugins/plugins/outlinepipeline-plugin.js';
|
import OutlinePipelinePlugin from "phaser3-rex-plugins/plugins/outlinepipeline-plugin.js";
|
||||||
import {EntryScene} from "./Phaser/Login/EntryScene";
|
import { EntryScene } from "./Phaser/Login/EntryScene";
|
||||||
import {coWebsiteManager} from "./WebRtc/CoWebsiteManager";
|
import { coWebsiteManager } from "./WebRtc/CoWebsiteManager";
|
||||||
import {MenuScene} from "./Phaser/Menu/MenuScene";
|
import { MenuScene } from "./Phaser/Menu/MenuScene";
|
||||||
import {localUserStore} from "./Connexion/LocalUserStore";
|
import { localUserStore } from "./Connexion/LocalUserStore";
|
||||||
import {ErrorScene} from "./Phaser/Reconnecting/ErrorScene";
|
import { ErrorScene } from "./Phaser/Reconnecting/ErrorScene";
|
||||||
import {iframeListener} from "./Api/IframeListener";
|
import { iframeListener } from "./Api/IframeListener";
|
||||||
import { SelectCharacterMobileScene } from './Phaser/Login/SelectCharacterMobileScene';
|
import { SelectCharacterMobileScene } from "./Phaser/Login/SelectCharacterMobileScene";
|
||||||
import {HdpiManager} from "./Phaser/Services/HdpiManager";
|
import { HdpiManager } from "./Phaser/Services/HdpiManager";
|
||||||
import {waScaleManager} from "./Phaser/Services/WaScaleManager";
|
import { waScaleManager } from "./Phaser/Services/WaScaleManager";
|
||||||
import {Game} from "./Phaser/Game/Game";
|
import { Game } from "./Phaser/Game/Game";
|
||||||
import App from './Components/App.svelte';
|
import App from "./Components/App.svelte";
|
||||||
import {HtmlUtils} from "./WebRtc/HtmlUtils";
|
import { HtmlUtils } from "./WebRtc/HtmlUtils";
|
||||||
import WebGLRenderer = Phaser.Renderer.WebGL.WebGLRenderer;
|
import WebGLRenderer = Phaser.Renderer.WebGL.WebGLRenderer;
|
||||||
|
|
||||||
|
const { width, height } = coWebsiteManager.getGameSize();
|
||||||
const {width, height} = coWebsiteManager.getGameSize();
|
|
||||||
|
|
||||||
const valueGameQuality = localUserStore.getGameQualityValue();
|
const valueGameQuality = localUserStore.getGameQualityValue();
|
||||||
const fps : Phaser.Types.Core.FPSConfig = {
|
const fps: Phaser.Types.Core.FPSConfig = {
|
||||||
/**
|
/**
|
||||||
* The minimum acceptable rendering rate, in frames per second.
|
* The minimum acceptable rendering rate, in frames per second.
|
||||||
*/
|
*/
|
||||||
@ -53,30 +52,30 @@ const fps : Phaser.Types.Core.FPSConfig = {
|
|||||||
/**
|
/**
|
||||||
* Apply delta smoothing during the game update to help avoid spikes?
|
* Apply delta smoothing during the game update to help avoid spikes?
|
||||||
*/
|
*/
|
||||||
smoothStep: false
|
smoothStep: false,
|
||||||
}
|
};
|
||||||
|
|
||||||
// the ?phaserMode=canvas parameter can be used to force Canvas usage
|
// the ?phaserMode=canvas parameter can be used to force Canvas usage
|
||||||
const params = new URLSearchParams(document.location.search.substring(1));
|
const params = new URLSearchParams(document.location.search.substring(1));
|
||||||
const phaserMode = params.get("phaserMode");
|
const phaserMode = params.get("phaserMode");
|
||||||
let mode: number;
|
let mode: number;
|
||||||
switch (phaserMode) {
|
switch (phaserMode) {
|
||||||
case 'auto':
|
case "auto":
|
||||||
case null:
|
case null:
|
||||||
mode = Phaser.AUTO;
|
mode = Phaser.AUTO;
|
||||||
break;
|
break;
|
||||||
case 'canvas':
|
case "canvas":
|
||||||
mode = Phaser.CANVAS;
|
mode = Phaser.CANVAS;
|
||||||
break;
|
break;
|
||||||
case 'webgl':
|
case "webgl":
|
||||||
mode = Phaser.WEBGL;
|
mode = Phaser.WEBGL;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new Error('phaserMode parameter must be one of "auto", "canvas" or "webgl"');
|
throw new Error('phaserMode parameter must be one of "auto", "canvas" or "webgl"');
|
||||||
}
|
}
|
||||||
|
|
||||||
const hdpiManager = new HdpiManager(640*480, 196*196);
|
const hdpiManager = new HdpiManager(640 * 480, 196 * 196);
|
||||||
const { game: gameSize, real: realSize } = hdpiManager.getOptimalGameSize({width, height});
|
const { game: gameSize, real: realSize } = hdpiManager.getOptimalGameSize({ width, height });
|
||||||
|
|
||||||
const config: GameConfig = {
|
const config: GameConfig = {
|
||||||
type: mode,
|
type: mode,
|
||||||
@ -87,9 +86,10 @@ const config: GameConfig = {
|
|||||||
height: gameSize.height,
|
height: gameSize.height,
|
||||||
zoom: realSize.width / gameSize.width,
|
zoom: realSize.width / gameSize.width,
|
||||||
autoRound: true,
|
autoRound: true,
|
||||||
resizeInterval: 999999999999
|
resizeInterval: 999999999999,
|
||||||
},
|
},
|
||||||
scene: [EntryScene,
|
scene: [
|
||||||
|
EntryScene,
|
||||||
LoginScene,
|
LoginScene,
|
||||||
isMobile() ? SelectCharacterMobileScene : SelectCharacterScene,
|
isMobile() ? SelectCharacterMobileScene : SelectCharacterScene,
|
||||||
SelectCompanionScene,
|
SelectCompanionScene,
|
||||||
@ -102,37 +102,39 @@ const config: GameConfig = {
|
|||||||
//resolution: window.devicePixelRatio / 2,
|
//resolution: window.devicePixelRatio / 2,
|
||||||
fps: fps,
|
fps: fps,
|
||||||
dom: {
|
dom: {
|
||||||
createContainer: true
|
createContainer: true,
|
||||||
},
|
},
|
||||||
render: {
|
render: {
|
||||||
pixelArt: true,
|
pixelArt: true,
|
||||||
roundPixels: true,
|
roundPixels: true,
|
||||||
antialias: false
|
antialias: false,
|
||||||
},
|
},
|
||||||
plugins: {
|
plugins: {
|
||||||
global: [{
|
global: [
|
||||||
key: 'rexWebFontLoader',
|
{
|
||||||
|
key: "rexWebFontLoader",
|
||||||
plugin: WebFontLoaderPlugin,
|
plugin: WebFontLoaderPlugin,
|
||||||
start: true
|
start: true,
|
||||||
}]
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
physics: {
|
physics: {
|
||||||
default: "arcade",
|
default: "arcade",
|
||||||
arcade: {
|
arcade: {
|
||||||
debug: DEBUG_MODE,
|
debug: DEBUG_MODE,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
// Instruct systems with 2 GPU to choose the low power one. We don't need that extra power and we want to save battery
|
// Instruct systems with 2 GPU to choose the low power one. We don't need that extra power and we want to save battery
|
||||||
powerPreference: "low-power",
|
powerPreference: "low-power",
|
||||||
callbacks: {
|
callbacks: {
|
||||||
postBoot: game => {
|
postBoot: (game) => {
|
||||||
// Install rexOutlinePipeline only if the renderer is WebGL.
|
// Install rexOutlinePipeline only if the renderer is WebGL.
|
||||||
const renderer = game.renderer;
|
const renderer = game.renderer;
|
||||||
if (renderer instanceof WebGLRenderer) {
|
if (renderer instanceof WebGLRenderer) {
|
||||||
game.plugins.install('rexOutlinePipeline', OutlinePipelinePlugin, true);
|
game.plugins.install("rexOutlinePipeline", OutlinePipelinePlugin, true);
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
//const game = new Phaser.Game(config);
|
//const game = new Phaser.Game(config);
|
||||||
@ -140,7 +142,7 @@ const game = new Game(config);
|
|||||||
|
|
||||||
waScaleManager.setGame(game);
|
waScaleManager.setGame(game);
|
||||||
|
|
||||||
window.addEventListener('resize', function (event) {
|
window.addEventListener("resize", function (event) {
|
||||||
coWebsiteManager.resetStyle();
|
coWebsiteManager.resetStyle();
|
||||||
|
|
||||||
waScaleManager.applyNewSize();
|
waScaleManager.applyNewSize();
|
||||||
@ -153,21 +155,22 @@ coWebsiteManager.onResize.subscribe(() => {
|
|||||||
iframeListener.init();
|
iframeListener.init();
|
||||||
|
|
||||||
const app = new App({
|
const app = new App({
|
||||||
target: HtmlUtils.getElementByIdOrFail('svelte-overlay'),
|
target: HtmlUtils.getElementByIdOrFail("svelte-overlay"),
|
||||||
props: {
|
props: {
|
||||||
game: game
|
game: game,
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
export default app
|
export default app;
|
||||||
|
|
||||||
if ('serviceWorker' in navigator) {
|
if ("serviceWorker" in navigator) {
|
||||||
window.addEventListener('load', function () {
|
window.addEventListener("load", function () {
|
||||||
navigator.serviceWorker.register('/resources/service-worker.js')
|
navigator.serviceWorker
|
||||||
.then(serviceWorker => {
|
.register("/resources/service-worker.js")
|
||||||
|
.then((serviceWorker) => {
|
||||||
console.log("Service Worker registered: ", serviceWorker);
|
console.log("Service Worker registered: ", serviceWorker);
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch((error) => {
|
||||||
console.error("Error registering the Service Worker: ", error);
|
console.error("Error registering the Service Worker: ", error);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,23 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<script>
|
|
||||||
var script = document.createElement('script');
|
|
||||||
// Don't do this at home kids! The "document.referrer" part is actually inserting a XSS security.
|
|
||||||
// We are OK in this precise case because the HTML page is hosted on the "maps" domain that contains only static files.
|
|
||||||
script.setAttribute('src', document.referrer + 'iframe_api.js');
|
|
||||||
document.head.appendChild(script);
|
|
||||||
window.addEventListener('load', () => {
|
|
||||||
WA.room.getCurrentRoom().then((room) => {
|
|
||||||
console.log('id : ', room.id);
|
|
||||||
console.log('map : ', room.map);
|
|
||||||
console.log('mapUrl : ', room.mapUrl);
|
|
||||||
console.log('startLayer : ', room.startLayer);
|
|
||||||
})
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<p>Log in the console the information of the current room</p>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
11
maps/tests/Metadata/getCurrentRoom.js
Normal file
11
maps/tests/Metadata/getCurrentRoom.js
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
WA.onInit().then(() => {
|
||||||
|
console.log('id: ', WA.room.id);
|
||||||
|
console.log('Map URL: ', WA.room.mapURL);
|
||||||
|
console.log('Player name: ', WA.player.name);
|
||||||
|
console.log('Player id: ', WA.player.id);
|
||||||
|
console.log('Player tags: ', WA.player.tags);
|
||||||
|
});
|
||||||
|
|
||||||
|
WA.room.getTiledMap().then((data) => {
|
||||||
|
console.log('Map data', data);
|
||||||
|
})
|
@ -1,11 +1,4 @@
|
|||||||
{ "compressionlevel":-1,
|
{ "compressionlevel":-1,
|
||||||
"editorsettings":
|
|
||||||
{
|
|
||||||
"export":
|
|
||||||
{
|
|
||||||
"target":"."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"height":10,
|
"height":10,
|
||||||
"infinite":false,
|
"infinite":false,
|
||||||
"layers":[
|
"layers":[
|
||||||
@ -51,29 +44,6 @@
|
|||||||
"x":0,
|
"x":0,
|
||||||
"y":0
|
"y":0
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"data":[0, 0, 0, 0, 0, 0, 128, 128, 128, 128, 0, 0, 0, 0, 0, 0, 128, 128, 128, 128, 0, 0, 0, 0, 0, 0, 128, 128, 128, 128, 0, 0, 0, 0, 0, 0, 128, 128, 128, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
|
||||||
"height":10,
|
|
||||||
"id":4,
|
|
||||||
"name":"metadata",
|
|
||||||
"opacity":1,
|
|
||||||
"properties":[
|
|
||||||
{
|
|
||||||
"name":"openWebsite",
|
|
||||||
"type":"string",
|
|
||||||
"value":"getCurrentRoom.html"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"openWebsiteAllowApi",
|
|
||||||
"type":"bool",
|
|
||||||
"value":true
|
|
||||||
}],
|
|
||||||
"type":"tilelayer",
|
|
||||||
"visible":true,
|
|
||||||
"width":10,
|
|
||||||
"x":0,
|
|
||||||
"y":0
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"draworder":"topdown",
|
"draworder":"topdown",
|
||||||
"id":5,
|
"id":5,
|
||||||
@ -88,7 +58,7 @@
|
|||||||
{
|
{
|
||||||
"fontfamily":"Sans Serif",
|
"fontfamily":"Sans Serif",
|
||||||
"pixelsize":9,
|
"pixelsize":9,
|
||||||
"text":"Test : \nWalk on the grass and open the console.\n\nResult : \nYou should see a console.log() of the following attributes : \n\t- id : ID of the current room\n\t- map : data of the JSON file of the map\n\t- mapUrl : url of the JSON file of the map\n\t- startLayer : Name of the layer where the current user started (HereYouAppered)\n\n\n",
|
"text":"Test : \nOpen the console.\n\nResult : \nYou should see a console.log() of the following attributes : \n\t- id : ID of the current room\n\t- mapUrl : url of the JSON file of the map\n\t- Player name\n - Player ID\n - Player tags\n\nAnd also:\n\t- map : data of the JSON file of the map\n\n",
|
||||||
"wrap":true
|
"wrap":true
|
||||||
},
|
},
|
||||||
"type":"",
|
"type":"",
|
||||||
@ -106,8 +76,14 @@
|
|||||||
"nextlayerid":11,
|
"nextlayerid":11,
|
||||||
"nextobjectid":2,
|
"nextobjectid":2,
|
||||||
"orientation":"orthogonal",
|
"orientation":"orthogonal",
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"script",
|
||||||
|
"type":"string",
|
||||||
|
"value":"getCurrentRoom.js"
|
||||||
|
}],
|
||||||
"renderorder":"right-down",
|
"renderorder":"right-down",
|
||||||
"tiledversion":"1.4.3",
|
"tiledversion":"2021.03.23",
|
||||||
"tileheight":32,
|
"tileheight":32,
|
||||||
"tilesets":[
|
"tilesets":[
|
||||||
{
|
{
|
||||||
@ -274,6 +250,6 @@
|
|||||||
}],
|
}],
|
||||||
"tilewidth":32,
|
"tilewidth":32,
|
||||||
"type":"map",
|
"type":"map",
|
||||||
"version":1.4,
|
"version":1.5,
|
||||||
"width":10
|
"width":10
|
||||||
}
|
}
|
@ -1,22 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<script>
|
|
||||||
var script = document.createElement('script');
|
|
||||||
// Don't do this at home kids! The "document.referrer" part is actually inserting a XSS security.
|
|
||||||
// We are OK in this precise case because the HTML page is hosted on the "maps" domain that contains only static files.
|
|
||||||
script.setAttribute('src', document.referrer + 'iframe_api.js');
|
|
||||||
document.head.appendChild(script);
|
|
||||||
window.addEventListener('load', () => {
|
|
||||||
WA.player.getCurrentUser().then((user) => {
|
|
||||||
console.log('id : ', user.id);
|
|
||||||
console.log('nickName : ', user.nickName);
|
|
||||||
console.log('tags : ', user.tags);
|
|
||||||
})
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<p>Log in the console the information of the current player</p>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -1,296 +0,0 @@
|
|||||||
{ "compressionlevel":-1,
|
|
||||||
"editorsettings":
|
|
||||||
{
|
|
||||||
"export":
|
|
||||||
{
|
|
||||||
"target":"."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"height":10,
|
|
||||||
"infinite":false,
|
|
||||||
"layers":[
|
|
||||||
{
|
|
||||||
"data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 52, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
|
||||||
"height":10,
|
|
||||||
"id":1,
|
|
||||||
"name":"start",
|
|
||||||
"opacity":1,
|
|
||||||
"type":"tilelayer",
|
|
||||||
"visible":true,
|
|
||||||
"width":10,
|
|
||||||
"x":0,
|
|
||||||
"y":0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"data":[33, 34, 34, 34, 34, 34, 34, 34, 34, 35, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 49, 50, 50, 50, 50, 50, 50, 50, 50, 51],
|
|
||||||
"height":10,
|
|
||||||
"id":2,
|
|
||||||
"name":"bottom",
|
|
||||||
"opacity":1,
|
|
||||||
"type":"tilelayer",
|
|
||||||
"visible":true,
|
|
||||||
"width":10,
|
|
||||||
"x":0,
|
|
||||||
"y":0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 101, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
|
||||||
"height":10,
|
|
||||||
"id":9,
|
|
||||||
"name":"exit",
|
|
||||||
"opacity":1,
|
|
||||||
"properties":[
|
|
||||||
{
|
|
||||||
"name":"exitUrl",
|
|
||||||
"type":"string",
|
|
||||||
"value":"getCurrentRoom.json#HereYouAppered"
|
|
||||||
}],
|
|
||||||
"type":"tilelayer",
|
|
||||||
"visible":true,
|
|
||||||
"width":10,
|
|
||||||
"x":0,
|
|
||||||
"y":0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"data":[0, 0, 0, 0, 0, 0, 128, 128, 128, 128, 0, 0, 0, 0, 0, 0, 128, 128, 128, 128, 0, 0, 0, 0, 0, 0, 128, 128, 128, 128, 0, 0, 0, 0, 0, 0, 128, 128, 128, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
|
||||||
"height":10,
|
|
||||||
"id":4,
|
|
||||||
"name":"metadata",
|
|
||||||
"opacity":1,
|
|
||||||
"properties":[
|
|
||||||
{
|
|
||||||
"name":"openWebsite",
|
|
||||||
"type":"string",
|
|
||||||
"value":"getCurrentUser.html"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"openWebsiteAllowApi",
|
|
||||||
"type":"bool",
|
|
||||||
"value":true
|
|
||||||
}],
|
|
||||||
"type":"tilelayer",
|
|
||||||
"visible":true,
|
|
||||||
"width":10,
|
|
||||||
"x":0,
|
|
||||||
"y":0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"draworder":"topdown",
|
|
||||||
"id":5,
|
|
||||||
"name":"floorLayer",
|
|
||||||
"objects":[
|
|
||||||
{
|
|
||||||
"height":151.839293303871,
|
|
||||||
"id":1,
|
|
||||||
"name":"",
|
|
||||||
"rotation":0,
|
|
||||||
"text":
|
|
||||||
{
|
|
||||||
"fontfamily":"Sans Serif",
|
|
||||||
"pixelsize":9,
|
|
||||||
"text":"Test : \nWalk on the grass, open the console.\n\nResut : \nYou should see a console.log() of the following attributes :\n\t- id : ID of the current user\n\t- nickName : Name of the current user\n\t- tags : List of tags of the current user\n\nFinally : \nWalk on the red tile and continue the test in an another room.",
|
|
||||||
"wrap":true
|
|
||||||
},
|
|
||||||
"type":"",
|
|
||||||
"visible":true,
|
|
||||||
"width":305.097705765524,
|
|
||||||
"x":14.750638909983,
|
|
||||||
"y":159.621625296353
|
|
||||||
}],
|
|
||||||
"opacity":1,
|
|
||||||
"type":"objectgroup",
|
|
||||||
"visible":true,
|
|
||||||
"x":0,
|
|
||||||
"y":0
|
|
||||||
}],
|
|
||||||
"nextlayerid":10,
|
|
||||||
"nextobjectid":2,
|
|
||||||
"orientation":"orthogonal",
|
|
||||||
"renderorder":"right-down",
|
|
||||||
"tiledversion":"1.4.3",
|
|
||||||
"tileheight":32,
|
|
||||||
"tilesets":[
|
|
||||||
{
|
|
||||||
"columns":8,
|
|
||||||
"firstgid":1,
|
|
||||||
"image":"tileset_dungeon.png",
|
|
||||||
"imageheight":256,
|
|
||||||
"imagewidth":256,
|
|
||||||
"margin":0,
|
|
||||||
"name":"TDungeon",
|
|
||||||
"spacing":0,
|
|
||||||
"tilecount":64,
|
|
||||||
"tileheight":32,
|
|
||||||
"tiles":[
|
|
||||||
{
|
|
||||||
"id":0,
|
|
||||||
"properties":[
|
|
||||||
{
|
|
||||||
"name":"collides",
|
|
||||||
"type":"bool",
|
|
||||||
"value":true
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id":1,
|
|
||||||
"properties":[
|
|
||||||
{
|
|
||||||
"name":"collides",
|
|
||||||
"type":"bool",
|
|
||||||
"value":true
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id":2,
|
|
||||||
"properties":[
|
|
||||||
{
|
|
||||||
"name":"collides",
|
|
||||||
"type":"bool",
|
|
||||||
"value":true
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id":3,
|
|
||||||
"properties":[
|
|
||||||
{
|
|
||||||
"name":"collides",
|
|
||||||
"type":"bool",
|
|
||||||
"value":true
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id":4,
|
|
||||||
"properties":[
|
|
||||||
{
|
|
||||||
"name":"collides",
|
|
||||||
"type":"bool",
|
|
||||||
"value":true
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id":8,
|
|
||||||
"properties":[
|
|
||||||
{
|
|
||||||
"name":"collides",
|
|
||||||
"type":"bool",
|
|
||||||
"value":true
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id":9,
|
|
||||||
"properties":[
|
|
||||||
{
|
|
||||||
"name":"collides",
|
|
||||||
"type":"bool",
|
|
||||||
"value":true
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id":10,
|
|
||||||
"properties":[
|
|
||||||
{
|
|
||||||
"name":"collides",
|
|
||||||
"type":"bool",
|
|
||||||
"value":true
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id":11,
|
|
||||||
"properties":[
|
|
||||||
{
|
|
||||||
"name":"collides",
|
|
||||||
"type":"bool",
|
|
||||||
"value":true
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id":12,
|
|
||||||
"properties":[
|
|
||||||
{
|
|
||||||
"name":"collides",
|
|
||||||
"type":"bool",
|
|
||||||
"value":true
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id":16,
|
|
||||||
"properties":[
|
|
||||||
{
|
|
||||||
"name":"collides",
|
|
||||||
"type":"bool",
|
|
||||||
"value":true
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id":17,
|
|
||||||
"properties":[
|
|
||||||
{
|
|
||||||
"name":"collides",
|
|
||||||
"type":"bool",
|
|
||||||
"value":true
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id":18,
|
|
||||||
"properties":[
|
|
||||||
{
|
|
||||||
"name":"collides",
|
|
||||||
"type":"bool",
|
|
||||||
"value":true
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id":19,
|
|
||||||
"properties":[
|
|
||||||
{
|
|
||||||
"name":"collides",
|
|
||||||
"type":"bool",
|
|
||||||
"value":true
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id":20,
|
|
||||||
"properties":[
|
|
||||||
{
|
|
||||||
"name":"collides",
|
|
||||||
"type":"bool",
|
|
||||||
"value":true
|
|
||||||
}]
|
|
||||||
}],
|
|
||||||
"tilewidth":32
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns":8,
|
|
||||||
"firstgid":65,
|
|
||||||
"image":"floortileset.png",
|
|
||||||
"imageheight":288,
|
|
||||||
"imagewidth":256,
|
|
||||||
"margin":0,
|
|
||||||
"name":"Floor",
|
|
||||||
"spacing":0,
|
|
||||||
"tilecount":72,
|
|
||||||
"tileheight":32,
|
|
||||||
"tiles":[
|
|
||||||
{
|
|
||||||
"animation":[
|
|
||||||
{
|
|
||||||
"duration":100,
|
|
||||||
"tileid":9
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"duration":100,
|
|
||||||
"tileid":64
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"duration":100,
|
|
||||||
"tileid":55
|
|
||||||
}],
|
|
||||||
"id":0
|
|
||||||
}],
|
|
||||||
"tilewidth":32
|
|
||||||
}],
|
|
||||||
"tilewidth":32,
|
|
||||||
"type":"map",
|
|
||||||
"version":1.4,
|
|
||||||
"width":10
|
|
||||||
}
|
|
33
maps/tests/Variables/script.js
Normal file
33
maps/tests/Variables/script.js
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
WA.onInit().then(() => {
|
||||||
|
console.log('Trying to read variable "doorOpened" whose default property is true. This should display "true".');
|
||||||
|
console.log('doorOpened', WA.state.loadVariable('doorOpened'));
|
||||||
|
|
||||||
|
console.log('Trying to set variable "not_exists". This should display an error in the console, followed by a log saying the error was caught.')
|
||||||
|
WA.state.saveVariable('not_exists', 'foo').catch((e) => {
|
||||||
|
console.log('Successfully caught error: ', e);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Trying to set variable "myvar". This should work.');
|
||||||
|
WA.state.saveVariable('myvar', {'foo': 'bar'});
|
||||||
|
|
||||||
|
console.log('Trying to read variable "myvar". This should display a {"foo": "bar"} object.');
|
||||||
|
console.log(WA.state.loadVariable('myvar'));
|
||||||
|
|
||||||
|
console.log('Trying to set variable "myvar" using proxy. This should work.');
|
||||||
|
WA.state.myvar = {'baz': 42};
|
||||||
|
|
||||||
|
console.log('Trying to read variable "myvar" using proxy. This should display a {"baz": 42} object.');
|
||||||
|
console.log(WA.state.myvar);
|
||||||
|
|
||||||
|
console.log('Trying to set variable "config". This should not work because we are not logged as admin.');
|
||||||
|
WA.state.saveVariable('config', {'foo': 'bar'}).catch(e => {
|
||||||
|
console.log('Successfully caught error because variable "config" is not writable: ', e);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Trying to read variable "readableByAdmin" that can only be read by "admin". We are not admin so we should not get the default value.');
|
||||||
|
if (WA.state.readableByAdmin === true) {
|
||||||
|
console.error('Failed test: readableByAdmin can be read.');
|
||||||
|
} else {
|
||||||
|
console.log('Success test: readableByAdmin was not read.');
|
||||||
|
}
|
||||||
|
});
|
48
maps/tests/Variables/shared_variables.html
Normal file
48
maps/tests/Variables/shared_variables.html
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<script>
|
||||||
|
var script = document.createElement('script');
|
||||||
|
// Don't do this at home kids! The "document.referrer" part is actually inserting a XSS security.
|
||||||
|
// We are OK in this precise case because the HTML page is hosted on the "maps" domain that contains only static files.
|
||||||
|
script.setAttribute('src', document.referrer + 'iframe_api.js');
|
||||||
|
document.head.appendChild(script);
|
||||||
|
window.addEventListener('load', () => {
|
||||||
|
console.log('On load');
|
||||||
|
WA.onInit().then(() => {
|
||||||
|
console.log('After WA init');
|
||||||
|
const textField = document.getElementById('textField');
|
||||||
|
textField.value = WA.state.textField;
|
||||||
|
|
||||||
|
textField.addEventListener('change', function (evt) {
|
||||||
|
console.log('saving variable')
|
||||||
|
WA.state.textField = this.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
WA.state.onVariableChange('textField').subscribe((value) => {
|
||||||
|
console.log('variable changed received')
|
||||||
|
textField.value = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('btn').addEventListener('click', () => {
|
||||||
|
console.log(WA.state.loadVariable('textField'));
|
||||||
|
document.getElementById('placeholder').innerText = WA.state.loadVariable('textField');
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('setUndefined').addEventListener('click', () => {
|
||||||
|
WA.state.textField = undefined;
|
||||||
|
document.getElementById('textField').value = '';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<input type="text" id="textField" />
|
||||||
|
|
||||||
|
<button id="setUndefined">Delete variable</button>
|
||||||
|
|
||||||
|
<button id="btn">Display textField variable value</button>
|
||||||
|
<div id="placeholder"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
131
maps/tests/Variables/shared_variables.json
Normal file
131
maps/tests/Variables/shared_variables.json
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
{ "compressionlevel":-1,
|
||||||
|
"height":10,
|
||||||
|
"infinite":false,
|
||||||
|
"layers":[
|
||||||
|
{
|
||||||
|
"data":[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||||
|
"height":10,
|
||||||
|
"id":1,
|
||||||
|
"name":"floor",
|
||||||
|
"opacity":1,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"openWebsite",
|
||||||
|
"type":"string",
|
||||||
|
"value":"shared_variables.html"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name":"openWebsiteAllowApi",
|
||||||
|
"type":"bool",
|
||||||
|
"value":true
|
||||||
|
}],
|
||||||
|
"type":"tilelayer",
|
||||||
|
"visible":true,
|
||||||
|
"width":10,
|
||||||
|
"x":0,
|
||||||
|
"y":0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||||
|
"height":10,
|
||||||
|
"id":2,
|
||||||
|
"name":"start",
|
||||||
|
"opacity":1,
|
||||||
|
"type":"tilelayer",
|
||||||
|
"visible":true,
|
||||||
|
"width":10,
|
||||||
|
"x":0,
|
||||||
|
"y":0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"draworder":"topdown",
|
||||||
|
"id":3,
|
||||||
|
"name":"floorLayer",
|
||||||
|
"objects":[
|
||||||
|
{
|
||||||
|
"height":67,
|
||||||
|
"id":3,
|
||||||
|
"name":"",
|
||||||
|
"rotation":0,
|
||||||
|
"text":
|
||||||
|
{
|
||||||
|
"fontfamily":"Sans Serif",
|
||||||
|
"pixelsize":11,
|
||||||
|
"text":"Test:\nChange the form\nConnect with another user\n\nResult:\nThe form should open in the same state for the other user\nAlso, a change on one user is directly propagated to the other user",
|
||||||
|
"wrap":true
|
||||||
|
},
|
||||||
|
"type":"",
|
||||||
|
"visible":true,
|
||||||
|
"width":252.4375,
|
||||||
|
"x":2.78125,
|
||||||
|
"y":2.5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"height":0,
|
||||||
|
"id":5,
|
||||||
|
"name":"textField",
|
||||||
|
"point":true,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"default",
|
||||||
|
"type":"string",
|
||||||
|
"value":"default value"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name":"jsonSchema",
|
||||||
|
"type":"string",
|
||||||
|
"value":"{}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name":"persist",
|
||||||
|
"type":"bool",
|
||||||
|
"value":true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name":"readableBy",
|
||||||
|
"type":"string",
|
||||||
|
"value":""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name":"writableBy",
|
||||||
|
"type":"string",
|
||||||
|
"value":""
|
||||||
|
}],
|
||||||
|
"rotation":0,
|
||||||
|
"type":"variable",
|
||||||
|
"visible":true,
|
||||||
|
"width":0,
|
||||||
|
"x":57.5,
|
||||||
|
"y":111
|
||||||
|
}],
|
||||||
|
"opacity":1,
|
||||||
|
"type":"objectgroup",
|
||||||
|
"visible":true,
|
||||||
|
"x":0,
|
||||||
|
"y":0
|
||||||
|
}],
|
||||||
|
"nextlayerid":8,
|
||||||
|
"nextobjectid":10,
|
||||||
|
"orientation":"orthogonal",
|
||||||
|
"renderorder":"right-down",
|
||||||
|
"tiledversion":"2021.03.23",
|
||||||
|
"tileheight":32,
|
||||||
|
"tilesets":[
|
||||||
|
{
|
||||||
|
"columns":11,
|
||||||
|
"firstgid":1,
|
||||||
|
"image":"..\/tileset1.png",
|
||||||
|
"imageheight":352,
|
||||||
|
"imagewidth":352,
|
||||||
|
"margin":0,
|
||||||
|
"name":"tileset1",
|
||||||
|
"spacing":0,
|
||||||
|
"tilecount":121,
|
||||||
|
"tileheight":32,
|
||||||
|
"tilewidth":32
|
||||||
|
}],
|
||||||
|
"tilewidth":32,
|
||||||
|
"type":"map",
|
||||||
|
"version":1.5,
|
||||||
|
"width":10
|
||||||
|
}
|
205
maps/tests/Variables/variables.json
Normal file
205
maps/tests/Variables/variables.json
Normal file
@ -0,0 +1,205 @@
|
|||||||
|
{ "compressionlevel":-1,
|
||||||
|
"height":10,
|
||||||
|
"infinite":false,
|
||||||
|
"layers":[
|
||||||
|
{
|
||||||
|
"data":[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||||
|
"height":10,
|
||||||
|
"id":1,
|
||||||
|
"name":"floor",
|
||||||
|
"opacity":1,
|
||||||
|
"type":"tilelayer",
|
||||||
|
"visible":true,
|
||||||
|
"width":10,
|
||||||
|
"x":0,
|
||||||
|
"y":0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||||
|
"height":10,
|
||||||
|
"id":2,
|
||||||
|
"name":"start",
|
||||||
|
"opacity":1,
|
||||||
|
"type":"tilelayer",
|
||||||
|
"visible":true,
|
||||||
|
"width":10,
|
||||||
|
"x":0,
|
||||||
|
"y":0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"draworder":"topdown",
|
||||||
|
"id":3,
|
||||||
|
"name":"floorLayer",
|
||||||
|
"objects":[
|
||||||
|
{
|
||||||
|
"height":67,
|
||||||
|
"id":3,
|
||||||
|
"name":"",
|
||||||
|
"rotation":0,
|
||||||
|
"text":
|
||||||
|
{
|
||||||
|
"fontfamily":"Sans Serif",
|
||||||
|
"pixelsize":11,
|
||||||
|
"text":"Test:\nOpen your console\n\nResult:\nYou should see a list of tests performed and results associated.",
|
||||||
|
"wrap":true
|
||||||
|
},
|
||||||
|
"type":"",
|
||||||
|
"visible":true,
|
||||||
|
"width":252.4375,
|
||||||
|
"x":2.78125,
|
||||||
|
"y":2.5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"height":0,
|
||||||
|
"id":5,
|
||||||
|
"name":"config",
|
||||||
|
"point":true,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"default",
|
||||||
|
"type":"string",
|
||||||
|
"value":"{}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name":"jsonSchema",
|
||||||
|
"type":"string",
|
||||||
|
"value":"{}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name":"persist",
|
||||||
|
"type":"bool",
|
||||||
|
"value":true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name":"readableBy",
|
||||||
|
"type":"string",
|
||||||
|
"value":""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name":"writableBy",
|
||||||
|
"type":"string",
|
||||||
|
"value":"admin"
|
||||||
|
}],
|
||||||
|
"rotation":0,
|
||||||
|
"type":"variable",
|
||||||
|
"visible":true,
|
||||||
|
"width":0,
|
||||||
|
"x":57.5,
|
||||||
|
"y":111
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"height":0,
|
||||||
|
"id":6,
|
||||||
|
"name":"doorOpened",
|
||||||
|
"point":true,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"default",
|
||||||
|
"type":"bool",
|
||||||
|
"value":true
|
||||||
|
}],
|
||||||
|
"rotation":0,
|
||||||
|
"type":"variable",
|
||||||
|
"visible":true,
|
||||||
|
"width":0,
|
||||||
|
"x":131.38069962269,
|
||||||
|
"y":106.004988169086
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"height":0,
|
||||||
|
"id":9,
|
||||||
|
"name":"myvar",
|
||||||
|
"point":true,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"default",
|
||||||
|
"type":"string",
|
||||||
|
"value":"{}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name":"jsonSchema",
|
||||||
|
"type":"string",
|
||||||
|
"value":"{}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name":"persist",
|
||||||
|
"type":"bool",
|
||||||
|
"value":true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name":"readableBy",
|
||||||
|
"type":"string",
|
||||||
|
"value":""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name":"writableBy",
|
||||||
|
"type":"string",
|
||||||
|
"value":""
|
||||||
|
}],
|
||||||
|
"rotation":0,
|
||||||
|
"type":"variable",
|
||||||
|
"visible":true,
|
||||||
|
"width":0,
|
||||||
|
"x":88.8149900876127,
|
||||||
|
"y":147.75212636695
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"height":0,
|
||||||
|
"id":10,
|
||||||
|
"name":"readableByAdmin",
|
||||||
|
"point":true,
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"default",
|
||||||
|
"type":"bool",
|
||||||
|
"value":true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name":"readableBy",
|
||||||
|
"type":"string",
|
||||||
|
"value":"admin"
|
||||||
|
}],
|
||||||
|
"rotation":0,
|
||||||
|
"type":"variable",
|
||||||
|
"visible":true,
|
||||||
|
"width":0,
|
||||||
|
"x":182.132122529897,
|
||||||
|
"y":157.984268082113
|
||||||
|
}],
|
||||||
|
"opacity":1,
|
||||||
|
"type":"objectgroup",
|
||||||
|
"visible":true,
|
||||||
|
"x":0,
|
||||||
|
"y":0
|
||||||
|
}],
|
||||||
|
"nextlayerid":8,
|
||||||
|
"nextobjectid":11,
|
||||||
|
"orientation":"orthogonal",
|
||||||
|
"properties":[
|
||||||
|
{
|
||||||
|
"name":"script",
|
||||||
|
"type":"string",
|
||||||
|
"value":"script.js"
|
||||||
|
}],
|
||||||
|
"renderorder":"right-down",
|
||||||
|
"tiledversion":"2021.03.23",
|
||||||
|
"tileheight":32,
|
||||||
|
"tilesets":[
|
||||||
|
{
|
||||||
|
"columns":11,
|
||||||
|
"firstgid":1,
|
||||||
|
"image":"..\/tileset1.png",
|
||||||
|
"imageheight":352,
|
||||||
|
"imagewidth":352,
|
||||||
|
"margin":0,
|
||||||
|
"name":"tileset1",
|
||||||
|
"spacing":0,
|
||||||
|
"tilecount":121,
|
||||||
|
"tileheight":32,
|
||||||
|
"tilewidth":32
|
||||||
|
}],
|
||||||
|
"tilewidth":32,
|
||||||
|
"type":"map",
|
||||||
|
"version":1.5,
|
||||||
|
"width":10
|
||||||
|
}
|
@ -127,15 +127,7 @@
|
|||||||
<input type="radio" name="test-getCurrentUser"> Success <input type="radio" name="test-getCurrentUser"> Failure <input type="radio" name="test-getCurrentUser" checked> Pending
|
<input type="radio" name="test-getCurrentUser"> Success <input type="radio" name="test-getCurrentUser"> Failure <input type="radio" name="test-getCurrentUser" checked> Pending
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="#" class="testLink" data-testmap="Metadata/getCurrentUser.json" target="_blank">Testing return current user attributes by Scripting API</a>
|
<a href="#" class="testLink" data-testmap="Metadata/getCurrentRoom.json" target="_blank">Testing return current player attributes in Scripting API + WA.onInit</a>
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<input type="radio" name="test-getCurrentRoom"> Success <input type="radio" name="test-getCurrentRoom"> Failure <input type="radio" name="test-getCurrentRoom" checked> Pending
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<a href="#" class="testLink" data-testmap="Metadata/getCurrentRoom.json" target="_blank">Testing return current room attributes by Scripting API (Need to test from current user)</a>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
@ -194,6 +186,22 @@
|
|||||||
<a href="#" class="testLink" data-testmap="Metadata/setTiles.json" target="_blank">Test set tiles</a>
|
<a href="#" class="testLink" data-testmap="Metadata/setTiles.json" target="_blank">Test set tiles</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<input type="radio" name="test-variables"> Success <input type="radio" name="test-variables"> Failure <input type="radio" name="test-variables" checked> Pending
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="#" class="testLink" data-testmap="Variables/variables.json" target="_blank">Testing scripting variables locally</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<input type="radio" name="test-shared-variables"> Success <input type="radio" name="test-shared-variables"> Failure <input type="radio" name="test-shared-variables" checked> Pending
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="#" class="testLink" data-testmap="Variables/shared_variables.json" target="_blank">Testing shared scripting variables</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
@ -94,6 +94,7 @@ message ClientToServerMessage {
|
|||||||
ReportPlayerMessage reportPlayerMessage = 11;
|
ReportPlayerMessage reportPlayerMessage = 11;
|
||||||
QueryJitsiJwtMessage queryJitsiJwtMessage = 12;
|
QueryJitsiJwtMessage queryJitsiJwtMessage = 12;
|
||||||
EmotePromptMessage emotePromptMessage = 13;
|
EmotePromptMessage emotePromptMessage = 13;
|
||||||
|
VariableMessage variableMessage = 14;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -107,6 +108,20 @@ message ItemEventMessage {
|
|||||||
string parametersJson = 4;
|
string parametersJson = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message VariableMessage {
|
||||||
|
string name = 1;
|
||||||
|
string value = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A variable, along the tag describing who it is targeted at
|
||||||
|
*/
|
||||||
|
message VariableWithTagMessage {
|
||||||
|
string name = 1;
|
||||||
|
string value = 2;
|
||||||
|
string readableBy = 3;
|
||||||
|
}
|
||||||
|
|
||||||
message PlayGlobalMessage {
|
message PlayGlobalMessage {
|
||||||
string id = 1;
|
string id = 1;
|
||||||
string type = 2;
|
string type = 2;
|
||||||
@ -133,6 +148,8 @@ message SubMessage {
|
|||||||
UserLeftMessage userLeftMessage = 5;
|
UserLeftMessage userLeftMessage = 5;
|
||||||
ItemEventMessage itemEventMessage = 6;
|
ItemEventMessage itemEventMessage = 6;
|
||||||
EmoteEventMessage emoteEventMessage = 7;
|
EmoteEventMessage emoteEventMessage = 7;
|
||||||
|
VariableMessage variableMessage = 8;
|
||||||
|
ErrorMessage errorMessage = 9;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -180,6 +197,7 @@ message RoomJoinedMessage {
|
|||||||
repeated ItemStateMessage item = 3;
|
repeated ItemStateMessage item = 3;
|
||||||
int32 currentUserId = 4;
|
int32 currentUserId = 4;
|
||||||
repeated string tag = 5;
|
repeated string tag = 5;
|
||||||
|
repeated VariableMessage variable = 6;
|
||||||
}
|
}
|
||||||
|
|
||||||
message WebRtcStartMessage {
|
message WebRtcStartMessage {
|
||||||
@ -318,6 +336,10 @@ message ZoneMessage {
|
|||||||
int32 y = 3;
|
int32 y = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message RoomMessage {
|
||||||
|
string roomId = 1;
|
||||||
|
}
|
||||||
|
|
||||||
message PusherToBackMessage {
|
message PusherToBackMessage {
|
||||||
oneof message {
|
oneof message {
|
||||||
JoinRoomMessage joinRoomMessage = 1;
|
JoinRoomMessage joinRoomMessage = 1;
|
||||||
@ -334,6 +356,7 @@ message PusherToBackMessage {
|
|||||||
SendUserMessage sendUserMessage = 12;
|
SendUserMessage sendUserMessage = 12;
|
||||||
BanUserMessage banUserMessage = 13;
|
BanUserMessage banUserMessage = 13;
|
||||||
EmotePromptMessage emotePromptMessage = 14;
|
EmotePromptMessage emotePromptMessage = 14;
|
||||||
|
VariableMessage variableMessage = 15;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -352,9 +375,22 @@ message SubToPusherMessage {
|
|||||||
SendUserMessage sendUserMessage = 7;
|
SendUserMessage sendUserMessage = 7;
|
||||||
BanUserMessage banUserMessage = 8;
|
BanUserMessage banUserMessage = 8;
|
||||||
EmoteEventMessage emoteEventMessage = 9;
|
EmoteEventMessage emoteEventMessage = 9;
|
||||||
|
ErrorMessage errorMessage = 10;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message BatchToPusherRoomMessage {
|
||||||
|
repeated SubToPusherRoomMessage payload = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message SubToPusherRoomMessage {
|
||||||
|
oneof message {
|
||||||
|
VariableWithTagMessage variableMessage = 1;
|
||||||
|
ErrorMessage errorMessage = 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/*message BatchToAdminPusherMessage {
|
/*message BatchToAdminPusherMessage {
|
||||||
repeated SubToAdminPusherMessage payload = 2;
|
repeated SubToAdminPusherMessage payload = 2;
|
||||||
}*/
|
}*/
|
||||||
@ -424,9 +460,13 @@ message EmptyMessage {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service handled by the "back". Pusher servers connect to this service.
|
||||||
|
*/
|
||||||
service RoomManager {
|
service RoomManager {
|
||||||
rpc joinRoom(stream PusherToBackMessage) returns (stream ServerToClientMessage);
|
rpc joinRoom(stream PusherToBackMessage) returns (stream ServerToClientMessage); // Holds a connection between one given client and the back
|
||||||
rpc listenZone(ZoneMessage) returns (stream BatchToPusherMessage);
|
rpc listenZone(ZoneMessage) returns (stream BatchToPusherMessage); // Connection used to send to a pusher messages related to a given zone of a given room
|
||||||
|
rpc listenRoom(RoomMessage) returns (stream BatchToPusherRoomMessage); // Connection used to send to a pusher messages related to a given room
|
||||||
rpc adminRoom(stream AdminPusherToBackMessage) returns (stream ServerToAdminClientMessage);
|
rpc adminRoom(stream AdminPusherToBackMessage) returns (stream ServerToAdminClientMessage);
|
||||||
rpc sendAdminMessage(AdminMessage) returns (EmptyMessage);
|
rpc sendAdminMessage(AdminMessage) returns (EmptyMessage);
|
||||||
rpc sendGlobalAdminMessage(AdminGlobalMessage) returns (EmptyMessage);
|
rpc sendGlobalAdminMessage(AdminGlobalMessage) returns (EmptyMessage);
|
||||||
|
@ -17,6 +17,7 @@ import {
|
|||||||
ServerToClientMessage,
|
ServerToClientMessage,
|
||||||
CompanionMessage,
|
CompanionMessage,
|
||||||
EmotePromptMessage,
|
EmotePromptMessage,
|
||||||
|
VariableMessage,
|
||||||
} from "../Messages/generated/messages_pb";
|
} from "../Messages/generated/messages_pb";
|
||||||
import { UserMovesMessage } from "../Messages/generated/messages_pb";
|
import { UserMovesMessage } from "../Messages/generated/messages_pb";
|
||||||
import { TemplatedApp } from "uWebSockets.js";
|
import { TemplatedApp } from "uWebSockets.js";
|
||||||
@ -357,6 +358,8 @@ export class IoSocketController {
|
|||||||
socketManager.handleSilentMessage(client, message.getSilentmessage() as SilentMessage);
|
socketManager.handleSilentMessage(client, message.getSilentmessage() as SilentMessage);
|
||||||
} else if (message.hasItemeventmessage()) {
|
} else if (message.hasItemeventmessage()) {
|
||||||
socketManager.handleItemEvent(client, message.getItemeventmessage() as ItemEventMessage);
|
socketManager.handleItemEvent(client, message.getItemeventmessage() as ItemEventMessage);
|
||||||
|
} else if (message.hasVariablemessage()) {
|
||||||
|
socketManager.handleVariableEvent(client, message.getVariablemessage() as VariableMessage);
|
||||||
} else if (message.hasWebrtcsignaltoservermessage()) {
|
} else if (message.hasWebrtcsignaltoservermessage()) {
|
||||||
socketManager.emitVideo(
|
socketManager.emitVideo(
|
||||||
client,
|
client,
|
||||||
|
@ -2,7 +2,29 @@ import { ExSocketInterface } from "_Model/Websocket/ExSocketInterface";
|
|||||||
import { PositionDispatcher } from "./PositionDispatcher";
|
import { PositionDispatcher } from "./PositionDispatcher";
|
||||||
import { ViewportInterface } from "_Model/Websocket/ViewportMessage";
|
import { ViewportInterface } from "_Model/Websocket/ViewportMessage";
|
||||||
import { arrayIntersect } from "../Services/ArrayHelper";
|
import { arrayIntersect } from "../Services/ArrayHelper";
|
||||||
import { ZoneEventListener } from "_Model/Zone";
|
import { GroupDescriptor, UserDescriptor, ZoneEventListener } from "_Model/Zone";
|
||||||
|
import { apiClientRepository } from "../Services/ApiClientRepository";
|
||||||
|
import {
|
||||||
|
BatchToPusherMessage,
|
||||||
|
BatchToPusherRoomMessage,
|
||||||
|
EmoteEventMessage,
|
||||||
|
ErrorMessage,
|
||||||
|
GroupLeftZoneMessage,
|
||||||
|
GroupUpdateZoneMessage,
|
||||||
|
RoomMessage,
|
||||||
|
SubMessage,
|
||||||
|
UserJoinedZoneMessage,
|
||||||
|
UserLeftZoneMessage,
|
||||||
|
UserMovedMessage,
|
||||||
|
VariableMessage,
|
||||||
|
VariableWithTagMessage,
|
||||||
|
ZoneMessage,
|
||||||
|
} from "../Messages/generated/messages_pb";
|
||||||
|
import Debug from "debug";
|
||||||
|
import { ClientReadableStream } from "grpc";
|
||||||
|
import { ExAdminSocketInterface } from "_Model/Websocket/ExAdminSocketInterface";
|
||||||
|
|
||||||
|
const debug = Debug("room");
|
||||||
|
|
||||||
export enum GameRoomPolicyTypes {
|
export enum GameRoomPolicyTypes {
|
||||||
ANONYMOUS_POLICY = 1,
|
ANONYMOUS_POLICY = 1,
|
||||||
@ -15,6 +37,10 @@ export class PusherRoom {
|
|||||||
public tags: string[];
|
public tags: string[];
|
||||||
public policyType: GameRoomPolicyTypes;
|
public policyType: GameRoomPolicyTypes;
|
||||||
private versionNumber: number = 1;
|
private versionNumber: number = 1;
|
||||||
|
private backConnection!: ClientReadableStream<BatchToPusherRoomMessage>;
|
||||||
|
private isClosing: boolean = false;
|
||||||
|
private listeners: Set<ExSocketInterface> = new Set<ExSocketInterface>();
|
||||||
|
//public readonly variables = new Map<string, string>();
|
||||||
|
|
||||||
constructor(public readonly roomUrl: string, private socketListener: ZoneEventListener) {
|
constructor(public readonly roomUrl: string, private socketListener: ZoneEventListener) {
|
||||||
this.tags = [];
|
this.tags = [];
|
||||||
@ -28,8 +54,13 @@ export class PusherRoom {
|
|||||||
this.positionNotifier.setViewport(socket, viewport);
|
this.positionNotifier.setViewport(socket, viewport);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public join(socket: ExSocketInterface) {
|
||||||
|
this.listeners.add(socket);
|
||||||
|
}
|
||||||
|
|
||||||
public leave(socket: ExSocketInterface) {
|
public leave(socket: ExSocketInterface) {
|
||||||
this.positionNotifier.removeViewport(socket);
|
this.positionNotifier.removeViewport(socket);
|
||||||
|
this.listeners.delete(socket);
|
||||||
}
|
}
|
||||||
|
|
||||||
public canAccess(userTags: string[]): boolean {
|
public canAccess(userTags: string[]): boolean {
|
||||||
@ -48,4 +79,75 @@ export class PusherRoom {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a connection to the back server to track global messages relative to this room (like variable changes).
|
||||||
|
*/
|
||||||
|
public async init(): Promise<void> {
|
||||||
|
debug("Opening connection to room %s on back server", this.roomUrl);
|
||||||
|
const apiClient = await apiClientRepository.getClient(this.roomUrl);
|
||||||
|
const roomMessage = new RoomMessage();
|
||||||
|
roomMessage.setRoomid(this.roomUrl);
|
||||||
|
this.backConnection = apiClient.listenRoom(roomMessage);
|
||||||
|
this.backConnection.on("data", (batch: BatchToPusherRoomMessage) => {
|
||||||
|
for (const message of batch.getPayloadList()) {
|
||||||
|
if (message.hasVariablemessage()) {
|
||||||
|
const variableMessage = message.getVariablemessage() as VariableWithTagMessage;
|
||||||
|
const readableBy = variableMessage.getReadableby();
|
||||||
|
|
||||||
|
// We need to store all variables to dispatch variables later to the listeners
|
||||||
|
//this.variables.set(variableMessage.getName(), variableMessage.getValue(), readableBy);
|
||||||
|
|
||||||
|
// Let's dispatch this variable to all the listeners
|
||||||
|
for (const listener of this.listeners) {
|
||||||
|
const subMessage = new SubMessage();
|
||||||
|
if (!readableBy || listener.tags.includes(readableBy)) {
|
||||||
|
subMessage.setVariablemessage(variableMessage);
|
||||||
|
}
|
||||||
|
listener.emitInBatch(subMessage);
|
||||||
|
}
|
||||||
|
} else if (message.hasErrormessage()) {
|
||||||
|
const errorMessage = message.getErrormessage() as ErrorMessage;
|
||||||
|
|
||||||
|
// Let's dispatch this error to all the listeners
|
||||||
|
for (const listener of this.listeners) {
|
||||||
|
const subMessage = new SubMessage();
|
||||||
|
subMessage.setErrormessage(errorMessage);
|
||||||
|
listener.emitInBatch(subMessage);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error("Unexpected message");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.backConnection.on("error", (e) => {
|
||||||
|
if (!this.isClosing) {
|
||||||
|
debug("Error on back connection");
|
||||||
|
this.close();
|
||||||
|
// Let's close all connections linked to that room
|
||||||
|
for (const listener of this.listeners) {
|
||||||
|
listener.disconnecting = true;
|
||||||
|
listener.end(1011, "Connection error between pusher and back server");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.backConnection.on("close", () => {
|
||||||
|
if (!this.isClosing) {
|
||||||
|
debug("Close on back connection");
|
||||||
|
this.close();
|
||||||
|
// Let's close all connections linked to that room
|
||||||
|
for (const listener of this.listeners) {
|
||||||
|
listener.disconnecting = true;
|
||||||
|
listener.end(1011, "Connection closed between pusher and back server");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public close(): void {
|
||||||
|
debug("Closing connection to room %s on back server", this.roomUrl);
|
||||||
|
this.isClosing = true;
|
||||||
|
this.backConnection.cancel();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,6 +15,7 @@ import {
|
|||||||
ZoneMessage,
|
ZoneMessage,
|
||||||
EmoteEventMessage,
|
EmoteEventMessage,
|
||||||
CompanionMessage,
|
CompanionMessage,
|
||||||
|
ErrorMessage,
|
||||||
} from "../Messages/generated/messages_pb";
|
} from "../Messages/generated/messages_pb";
|
||||||
import { ClientReadableStream } from "grpc";
|
import { ClientReadableStream } from "grpc";
|
||||||
import { PositionDispatcher } from "_Model/PositionDispatcher";
|
import { PositionDispatcher } from "_Model/PositionDispatcher";
|
||||||
@ -30,6 +31,7 @@ export interface ZoneEventListener {
|
|||||||
onGroupMoves(group: GroupDescriptor, listener: ExSocketInterface): void;
|
onGroupMoves(group: GroupDescriptor, listener: ExSocketInterface): void;
|
||||||
onGroupLeaves(groupId: number, listener: ExSocketInterface): void;
|
onGroupLeaves(groupId: number, listener: ExSocketInterface): void;
|
||||||
onEmote(emoteMessage: EmoteEventMessage, listener: ExSocketInterface): void;
|
onEmote(emoteMessage: EmoteEventMessage, listener: ExSocketInterface): void;
|
||||||
|
onError(errorMessage: ErrorMessage, listener: ExSocketInterface): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*export type EntersCallback = (thing: Movable, listener: User) => void;
|
/*export type EntersCallback = (thing: Movable, listener: User) => void;
|
||||||
@ -217,6 +219,9 @@ export class Zone {
|
|||||||
} else if (message.hasEmoteeventmessage()) {
|
} else if (message.hasEmoteeventmessage()) {
|
||||||
const emoteEventMessage = message.getEmoteeventmessage() as EmoteEventMessage;
|
const emoteEventMessage = message.getEmoteeventmessage() as EmoteEventMessage;
|
||||||
this.notifyEmote(emoteEventMessage);
|
this.notifyEmote(emoteEventMessage);
|
||||||
|
} else if (message.hasErrormessage()) {
|
||||||
|
const errorMessage = message.getErrormessage() as ErrorMessage;
|
||||||
|
this.notifyError(errorMessage);
|
||||||
} else {
|
} else {
|
||||||
throw new Error("Unexpected message");
|
throw new Error("Unexpected message");
|
||||||
}
|
}
|
||||||
@ -303,6 +308,12 @@ export class Zone {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private notifyError(errorMessage: ErrorMessage) {
|
||||||
|
for (const listener of this.listeners) {
|
||||||
|
this.socketListener.onError(errorMessage, listener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Notify listeners of this zone that this group left
|
* Notify listeners of this zone that this group left
|
||||||
*/
|
*/
|
||||||
|
@ -30,6 +30,8 @@ import {
|
|||||||
BanMessage,
|
BanMessage,
|
||||||
RefreshRoomMessage,
|
RefreshRoomMessage,
|
||||||
EmotePromptMessage,
|
EmotePromptMessage,
|
||||||
|
VariableMessage,
|
||||||
|
ErrorMessage,
|
||||||
} from "../Messages/generated/messages_pb";
|
} from "../Messages/generated/messages_pb";
|
||||||
import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils";
|
import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils";
|
||||||
import { ADMIN_API_URL, JITSI_ISS, SECRET_JITSI_KEY } from "../Enum/EnvironmentVariable";
|
import { ADMIN_API_URL, JITSI_ISS, SECRET_JITSI_KEY } from "../Enum/EnvironmentVariable";
|
||||||
@ -226,6 +228,9 @@ export class SocketManager implements ZoneEventListener {
|
|||||||
const pusherToBackMessage = new PusherToBackMessage();
|
const pusherToBackMessage = new PusherToBackMessage();
|
||||||
pusherToBackMessage.setJoinroommessage(joinRoomMessage);
|
pusherToBackMessage.setJoinroommessage(joinRoomMessage);
|
||||||
streamToPusher.write(pusherToBackMessage);
|
streamToPusher.write(pusherToBackMessage);
|
||||||
|
|
||||||
|
const pusherRoom = await this.getOrCreateRoom(client.roomId);
|
||||||
|
pusherRoom.join(client);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('An error occurred on "join_room" event');
|
console.error('An error occurred on "join_room" event');
|
||||||
console.error(e);
|
console.error(e);
|
||||||
@ -277,6 +282,13 @@ export class SocketManager implements ZoneEventListener {
|
|||||||
emitInBatch(listener, subMessage);
|
emitInBatch(listener, subMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onError(errorMessage: ErrorMessage, listener: ExSocketInterface): void {
|
||||||
|
const subMessage = new SubMessage();
|
||||||
|
subMessage.setErrormessage(errorMessage);
|
||||||
|
|
||||||
|
emitInBatch(listener, subMessage);
|
||||||
|
}
|
||||||
|
|
||||||
// Useless now, will be useful again if we allow editing details in game
|
// Useless now, will be useful again if we allow editing details in game
|
||||||
handleSetPlayerDetails(client: ExSocketInterface, playerDetailsMessage: SetPlayerDetailsMessage) {
|
handleSetPlayerDetails(client: ExSocketInterface, playerDetailsMessage: SetPlayerDetailsMessage) {
|
||||||
const pusherToBackMessage = new PusherToBackMessage();
|
const pusherToBackMessage = new PusherToBackMessage();
|
||||||
@ -299,6 +311,13 @@ export class SocketManager implements ZoneEventListener {
|
|||||||
client.backConnection.write(pusherToBackMessage);
|
client.backConnection.write(pusherToBackMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleVariableEvent(client: ExSocketInterface, variableMessage: VariableMessage) {
|
||||||
|
const pusherToBackMessage = new PusherToBackMessage();
|
||||||
|
pusherToBackMessage.setVariablemessage(variableMessage);
|
||||||
|
|
||||||
|
client.backConnection.write(pusherToBackMessage);
|
||||||
|
}
|
||||||
|
|
||||||
async handleReportMessage(client: ExSocketInterface, reportPlayerMessage: ReportPlayerMessage) {
|
async handleReportMessage(client: ExSocketInterface, reportPlayerMessage: ReportPlayerMessage) {
|
||||||
try {
|
try {
|
||||||
await adminApi.reportPlayer(
|
await adminApi.reportPlayer(
|
||||||
@ -339,6 +358,7 @@ export class SocketManager implements ZoneEventListener {
|
|||||||
|
|
||||||
room.leave(socket);
|
room.leave(socket);
|
||||||
if (room.isEmpty()) {
|
if (room.isEmpty()) {
|
||||||
|
room.close();
|
||||||
this.rooms.delete(socket.roomId);
|
this.rooms.delete(socket.roomId);
|
||||||
debug("Room %s is empty. Deleting.", socket.roomId);
|
debug("Room %s is empty. Deleting.", socket.roomId);
|
||||||
}
|
}
|
||||||
@ -368,7 +388,7 @@ export class SocketManager implements ZoneEventListener {
|
|||||||
if (ADMIN_API_URL) {
|
if (ADMIN_API_URL) {
|
||||||
await this.updateRoomWithAdminData(room);
|
await this.updateRoomWithAdminData(room);
|
||||||
}
|
}
|
||||||
|
await room.init();
|
||||||
this.rooms.set(roomUrl, room);
|
this.rooms.set(roomUrl, room);
|
||||||
}
|
}
|
||||||
return room;
|
return room;
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
"experimentalDecorators": true,
|
"experimentalDecorators": true,
|
||||||
/* Basic Options */
|
/* Basic Options */
|
||||||
// "incremental": true, /* Enable incremental compilation */
|
// "incremental": true, /* Enable incremental compilation */
|
||||||
"target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
|
"target": "ES2019", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
|
||||||
"downlevelIteration": true,
|
"downlevelIteration": true,
|
||||||
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
|
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
|
||||||
// "lib": [], /* Specify library files to be included in the compilation. */
|
// "lib": [], /* Specify library files to be included in the compilation. */
|
||||||
|
Loading…
Reference in New Issue
Block a user