Adding support for "readableBy" and "writableBy" in back
This means that we are now loading maps from server side.
This commit is contained in:
parent
3d76f76d3e
commit
dbd5b80636
@ -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,6 +48,7 @@
|
|||||||
"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",
|
||||||
|
@ -11,39 +11,54 @@ import {
|
|||||||
EmoteEventMessage,
|
EmoteEventMessage,
|
||||||
JoinRoomMessage,
|
JoinRoomMessage,
|
||||||
SubToPusherRoomMessage,
|
SubToPusherRoomMessage,
|
||||||
VariableMessage,
|
VariableMessage, VariableWithTagMessage,
|
||||||
} from "../Messages/generated/messages_pb";
|
} from "../Messages/generated/messages_pb";
|
||||||
import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils";
|
import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils";
|
||||||
import { RoomSocket, 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";
|
||||||
|
|
||||||
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 readonly disconnectCallback: DisconnectCallback;
|
|
||||||
|
|
||||||
private itemsState = new Map<number, unknown>();
|
private itemsState = new Map<number, unknown>();
|
||||||
public readonly variables = new Map<string, string>();
|
|
||||||
|
|
||||||
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;
|
||||||
|
|
||||||
private roomListeners: Set<RoomSocket> = new Set<RoomSocket>();
|
private roomListeners: Set<RoomSocket> = new Set<RoomSocket>();
|
||||||
|
|
||||||
constructor(
|
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,
|
||||||
@ -53,19 +68,12 @@ 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(roomUrl, mapDetails.mapUrl, connectCallback, disconnectCallback, minDistance, groupRadius, onEnters, onMoves, onLeaves, onEmote);
|
||||||
this.usersByUuid = new Map<string, User>();
|
|
||||||
this.admins = new Set<Admin>();
|
return gameRoom;
|
||||||
this.groups = new Set<Group>();
|
|
||||||
this.connectCallback = connectCallback;
|
|
||||||
this.disconnectCallback = disconnectCallback;
|
|
||||||
this.minDistance = minDistance;
|
|
||||||
this.groupRadius = groupRadius;
|
|
||||||
// A zone is 10 sprites wide.
|
|
||||||
this.positionNotifier = new PositionNotifier(320, 320, onEnters, onMoves, onLeaves, onEmote);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public getGroups(): Group[] {
|
public getGroups(): Group[] {
|
||||||
@ -299,13 +307,19 @@ export class GameRoom {
|
|||||||
return this.itemsState;
|
return this.itemsState;
|
||||||
}
|
}
|
||||||
|
|
||||||
public setVariable(name: string, value: string): void {
|
public async setVariable(name: string, value: string, user: User): Promise<void> {
|
||||||
this.variables.set(name, value);
|
// 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?
|
// TODO: should we batch those every 100ms?
|
||||||
const variableMessage = new VariableMessage();
|
const variableMessage = new VariableWithTagMessage();
|
||||||
variableMessage.setName(name);
|
variableMessage.setName(name);
|
||||||
variableMessage.setValue(value);
|
variableMessage.setValue(value);
|
||||||
|
if (readableBy) {
|
||||||
|
variableMessage.setReadableby(readableBy);
|
||||||
|
}
|
||||||
|
|
||||||
const subMessage = new SubToPusherRoomMessage();
|
const subMessage = new SubToPusherRoomMessage();
|
||||||
subMessage.setVariablemessage(variableMessage);
|
subMessage.setVariablemessage(variableMessage);
|
||||||
@ -356,4 +370,82 @@ export class GameRoom {
|
|||||||
public removeRoomListener(socket: RoomSocket) {
|
public removeRoomListener(socket: RoomSocket) {
|
||||||
this.roomListeners.delete(socket);
|
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 = new Promise<VariablesManager>((resolve, reject) => {
|
||||||
|
this.getMap().then((map) => {
|
||||||
|
resolve(new VariablesManager(map));
|
||||||
|
}).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.
|
||||||
|
|
||||||
|
// FIXME: find a way to send a warning to the client side
|
||||||
|
// FIXME: find a way to send a warning to the client side
|
||||||
|
// FIXME: find a way to send a warning to the client side
|
||||||
|
// FIXME: find a way to send a warning to the client side
|
||||||
|
resolve(new VariablesManager(null));
|
||||||
|
} else {
|
||||||
|
reject(e);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return this.variableManagerPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getVariablesForTags(tags: string[]): Promise<Map<string, string>> {
|
||||||
|
const variablesManager = await this.getVariableManager();
|
||||||
|
return variablesManager.getVariablesForTags(tags);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@ import {
|
|||||||
AdminMessage,
|
AdminMessage,
|
||||||
AdminPusherToBackMessage,
|
AdminPusherToBackMessage,
|
||||||
AdminRoomMessage,
|
AdminRoomMessage,
|
||||||
BanMessage, BatchToPusherRoomMessage,
|
BanMessage, BatchToPusherMessage, BatchToPusherRoomMessage,
|
||||||
EmotePromptMessage,
|
EmotePromptMessage,
|
||||||
EmptyMessage,
|
EmptyMessage,
|
||||||
ItemEventMessage,
|
ItemEventMessage,
|
||||||
@ -14,7 +14,6 @@ import {
|
|||||||
QueryJitsiJwtMessage,
|
QueryJitsiJwtMessage,
|
||||||
RefreshRoomPromptMessage, RoomMessage,
|
RefreshRoomPromptMessage, RoomMessage,
|
||||||
ServerToAdminClientMessage,
|
ServerToAdminClientMessage,
|
||||||
ServerToClientMessage,
|
|
||||||
SilentMessage,
|
SilentMessage,
|
||||||
UserMovesMessage, VariableMessage,
|
UserMovesMessage, VariableMessage,
|
||||||
WebRtcSignalToServerMessage,
|
WebRtcSignalToServerMessage,
|
||||||
@ -23,7 +22,7 @@ import {
|
|||||||
} 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 +31,7 @@ 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>;
|
export type RoomSocket = ServerWritableStream<RoomMessage, BatchToPusherRoomMessage>;
|
||||||
|
|
||||||
const roomManager: IRoomManagerServer = {
|
const roomManager: IRoomManagerServer = {
|
||||||
@ -56,7 +55,7 @@ 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");
|
||||||
}
|
}
|
||||||
@ -139,20 +138,22 @@ 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();
|
call.end();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -161,20 +162,22 @@ const roomManager: IRoomManagerServer = {
|
|||||||
debug("listenRoom called");
|
debug("listenRoom called");
|
||||||
const roomMessage = call.request;
|
const roomMessage = call.request;
|
||||||
|
|
||||||
socketManager.addRoomListener(call, roomMessage.getRoomid());
|
socketManager.addRoomListener(call, roomMessage.getRoomid()).catch(e => {
|
||||||
|
emitErrorOnRoomSocket(call, e.toString());
|
||||||
|
});
|
||||||
|
|
||||||
call.on("cancelled", () => {
|
call.on("cancelled", () => {
|
||||||
debug("listenRoom cancelled");
|
debug("listenRoom cancelled");
|
||||||
socketManager.removeRoomListener(call, roomMessage.getRoomid());
|
socketManager.removeRoomListener(call, roomMessage.getRoomid()).catch(e => console.error(e));
|
||||||
call.end();
|
call.end();
|
||||||
});
|
});
|
||||||
|
|
||||||
call.on("close", () => {
|
call.on("close", () => {
|
||||||
debug("listenRoom connection closed");
|
debug("listenRoom connection closed");
|
||||||
socketManager.removeRoomListener(call, roomMessage.getRoomid());
|
socketManager.removeRoomListener(call, roomMessage.getRoomid()).catch(e => console.error(e));
|
||||||
}).on("error", (e) => {
|
}).on("error", (e) => {
|
||||||
console.error("An error occurred in listenRoom stream:", e);
|
console.error("An error occurred in listenRoom stream:", e);
|
||||||
socketManager.removeRoomListener(call, roomMessage.getRoomid());
|
socketManager.removeRoomListener(call, roomMessage.getRoomid()).catch(e => console.error(e));
|
||||||
call.end();
|
call.end();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -193,7 +196,7 @@ const roomManager: IRoomManagerServer = {
|
|||||||
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");
|
||||||
}
|
}
|
||||||
@ -222,7 +225,7 @@ const roomManager: IRoomManagerServer = {
|
|||||||
call.request.getRoomid(),
|
call.request.getRoomid(),
|
||||||
call.request.getRecipientuuid(),
|
call.request.getRecipientuuid(),
|
||||||
call.request.getMessage()
|
call.request.getMessage()
|
||||||
);
|
).catch(e => console.error(e));
|
||||||
|
|
||||||
callback(null, new EmptyMessage());
|
callback(null, new EmptyMessage());
|
||||||
},
|
},
|
||||||
@ -233,26 +236,29 @@ 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>;
|
2
back/src/Services/LocalUrlError.ts
Normal file
2
back/src/Services/LocalUrlError.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export class LocalUrlError extends Error {
|
||||||
|
}
|
64
back/src/Services/MapFetcher.ts
Normal file
64
back/src/Services/MapFetcher.ts
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
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
|
||||||
|
});
|
||||||
|
|
||||||
|
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)(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,11 @@
|
|||||||
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 +19,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);
|
||||||
|
}
|
||||||
|
@ -68,7 +68,7 @@ function emitZoneMessage(subMessage: SubToPusherMessage, socket: ZoneSocket): vo
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class SocketManager {
|
export class SocketManager {
|
||||||
private rooms = new Map<string, GameRoom>();
|
//private rooms = new Map<string, GameRoom>();
|
||||||
// List of rooms in process of loading.
|
// List of rooms in process of loading.
|
||||||
private roomsPromises = new Map<string, PromiseLike<GameRoom>>();
|
private roomsPromises = new Map<string, PromiseLike<GameRoom>>();
|
||||||
|
|
||||||
@ -106,7 +106,9 @@ export class SocketManager {
|
|||||||
roomJoinedMessage.addItem(itemStateMessage);
|
roomJoinedMessage.addItem(itemStateMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const [name, value] of room.variables.entries()) {
|
const variables = await room.getVariablesForTags(user.tags);
|
||||||
|
|
||||||
|
for (const [name, value] of variables.entries()) {
|
||||||
const variableMessage = new VariableMessage();
|
const variableMessage = new VariableMessage();
|
||||||
variableMessage.setName(name);
|
variableMessage.setName(name);
|
||||||
variableMessage.setValue(value);
|
variableMessage.setValue(value);
|
||||||
@ -198,12 +200,14 @@ export class SocketManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleVariableEvent(room: GameRoom, user: User, variableMessage: VariableMessage) {
|
handleVariableEvent(room: GameRoom, user: User, variableMessage: VariableMessage) {
|
||||||
try {
|
(async () => {
|
||||||
room.setVariable(variableMessage.getName(), variableMessage.getValue());
|
try {
|
||||||
} catch (e) {
|
await room.setVariable(variableMessage.getName(), variableMessage.getValue(), user);
|
||||||
console.error('An error occurred on "handleVariableEvent"');
|
} catch (e) {
|
||||||
console.error(e);
|
console.error('An error occurred on "handleVariableEvent"');
|
||||||
}
|
console.error(e);
|
||||||
|
}
|
||||||
|
})();
|
||||||
}
|
}
|
||||||
|
|
||||||
emitVideo(room: GameRoom, user: User, data: WebRtcSignalToServerMessage): void {
|
emitVideo(room: GameRoom, user: User, data: WebRtcSignalToServerMessage): void {
|
||||||
@ -272,7 +276,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);
|
||||||
}
|
}
|
||||||
@ -284,38 +288,34 @@ export class SocketManager {
|
|||||||
|
|
||||||
async getOrCreateRoom(roomId: string): Promise<GameRoom> {
|
async getOrCreateRoom(roomId: string): Promise<GameRoom> {
|
||||||
//check and create new room
|
//check and create new room
|
||||||
let room = this.rooms.get(roomId);
|
let roomPromise = this.roomsPromises.get(roomId);
|
||||||
if (room === undefined) {
|
if (roomPromise === undefined) {
|
||||||
let roomPromise = this.roomsPromises.get(roomId);
|
roomPromise = new Promise<GameRoom>((resolve, reject) => {
|
||||||
if (roomPromise) {
|
GameRoom.create(
|
||||||
return roomPromise;
|
roomId,
|
||||||
}
|
(user: User, group: Group) => this.joinWebRtcRoom(user, group),
|
||||||
|
(user: User, group: Group) => this.disConnectedUser(user, group),
|
||||||
// Note: for now, the promise is useless (because this is synchronous, but soon, we will need to
|
MINIMUM_DISTANCE,
|
||||||
// load the map server side.
|
GROUP_RADIUS,
|
||||||
|
(thing: Movable, fromZone: Zone | null, listener: ZoneSocket) =>
|
||||||
room = new GameRoom(
|
this.onZoneEnter(thing, fromZone, listener),
|
||||||
roomId,
|
(thing: Movable, position: PositionInterface, listener: ZoneSocket) =>
|
||||||
(user: User, group: Group) => this.joinWebRtcRoom(user, group),
|
this.onClientMove(thing, position, listener),
|
||||||
(user: User, group: Group) => this.disConnectedUser(user, group),
|
(thing: Movable, newZone: Zone | null, listener: ZoneSocket) =>
|
||||||
MINIMUM_DISTANCE,
|
this.onClientLeave(thing, newZone, listener),
|
||||||
GROUP_RADIUS,
|
(emoteEventMessage: EmoteEventMessage, listener: ZoneSocket) =>
|
||||||
(thing: Movable, fromZone: Zone | null, listener: ZoneSocket) =>
|
this.onEmote(emoteEventMessage, listener)
|
||||||
this.onZoneEnter(thing, fromZone, listener),
|
).then((gameRoom) => {
|
||||||
(thing: Movable, position: PositionInterface, listener: ZoneSocket) =>
|
gaugeManager.incNbRoomGauge();
|
||||||
this.onClientMove(thing, position, listener),
|
resolve(gameRoom);
|
||||||
(thing: Movable, newZone: Zone | null, listener: ZoneSocket) =>
|
}).catch((e) => {
|
||||||
this.onClientLeave(thing, newZone, listener),
|
this.roomsPromises.delete(roomId);
|
||||||
(emoteEventMessage: EmoteEventMessage, listener: ZoneSocket) =>
|
reject(e);
|
||||||
this.onEmote(emoteEventMessage, listener)
|
});
|
||||||
);
|
});
|
||||||
gaugeManager.incNbRoomGauge();
|
this.roomsPromises.set(roomId, roomPromise);
|
||||||
this.rooms.set(roomId, room);
|
|
||||||
|
|
||||||
// TODO: change this the to new Promise()... when the method becomes actually asynchronous
|
|
||||||
roomPromise = Promise.resolve(room);
|
|
||||||
}
|
}
|
||||||
return Promise.resolve(room);
|
return roomPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async joinRoom(
|
private async joinRoom(
|
||||||
@ -554,8 +554,8 @@ export class SocketManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
||||||
@ -625,11 +625,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);
|
||||||
@ -670,11 +669,10 @@ 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);
|
||||||
@ -683,8 +681,7 @@ export class SocketManager {
|
|||||||
async addRoomListener(call: RoomSocket, roomId: string) {
|
async addRoomListener(call: RoomSocket, roomId: string) {
|
||||||
const room = await this.getOrCreateRoom(roomId);
|
const room = await this.getOrCreateRoom(roomId);
|
||||||
if (!room) {
|
if (!room) {
|
||||||
console.error("In addRoomListener, could not find room with id '" + roomId + "'");
|
throw new Error("In addRoomListener, could not find room with id '" + roomId + "'");
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
room.addRoomListener(call);
|
room.addRoomListener(call);
|
||||||
@ -692,7 +689,10 @@ export class SocketManager {
|
|||||||
|
|
||||||
const batchMessage = new BatchToPusherRoomMessage();
|
const batchMessage = new BatchToPusherRoomMessage();
|
||||||
|
|
||||||
for (const [name, value] of room.variables.entries()) {
|
// Finally, no need to store variables in the pusher, let's only make it act as a relay
|
||||||
|
/*const variables = await room.getVariables();
|
||||||
|
|
||||||
|
for (const [name, value] of variables.entries()) {
|
||||||
const variableMessage = new VariableMessage();
|
const variableMessage = new VariableMessage();
|
||||||
variableMessage.setName(name);
|
variableMessage.setName(name);
|
||||||
variableMessage.setValue(value);
|
variableMessage.setValue(value);
|
||||||
@ -701,16 +701,15 @@ export class SocketManager {
|
|||||||
subMessage.setVariablemessage(variableMessage);
|
subMessage.setVariablemessage(variableMessage);
|
||||||
|
|
||||||
batchMessage.addPayload(subMessage);
|
batchMessage.addPayload(subMessage);
|
||||||
}
|
}*/
|
||||||
|
|
||||||
call.write(batchMessage);
|
call.write(batchMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
removeRoomListener(call: RoomSocket, roomId: string) {
|
async removeRoomListener(call: RoomSocket, roomId: string) {
|
||||||
const room = this.rooms.get(roomId);
|
const room = await this.roomsPromises.get(roomId);
|
||||||
if (!room) {
|
if (!room) {
|
||||||
console.error("In removeRoomListener, could not find room with id '" + roomId + "'");
|
throw new Error("In removeRoomListener, could not find room with id '" + roomId + "'");
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
room.removeRoomListener(call);
|
room.removeRoomListener(call);
|
||||||
@ -727,14 +726,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 '" +
|
||||||
@ -764,8 +763,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 '" +
|
||||||
@ -800,8 +799,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(
|
||||||
@ -824,8 +823,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(
|
||||||
@ -846,8 +845,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;
|
||||||
}
|
}
|
||||||
|
139
back/src/Services/VariablesManager.ts
Normal file
139
back/src/Services/VariablesManager.ts
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
/**
|
||||||
|
* 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";
|
||||||
|
|
||||||
|
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 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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.indexOf(variableObject.writableBy) === -1) {
|
||||||
|
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);
|
||||||
|
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.indexOf(variableObject.readableBy) !== -1) {
|
||||||
|
readableVariables.set(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return readableVariables;
|
||||||
|
}
|
||||||
|
}
|
@ -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. */
|
||||||
|
@ -187,6 +187,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"
|
||||||
@ -1181,6 +1188,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 +1429,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"
|
||||||
|
@ -32,7 +32,7 @@ import {
|
|||||||
EmotePromptMessage,
|
EmotePromptMessage,
|
||||||
SendUserMessage,
|
SendUserMessage,
|
||||||
BanUserMessage,
|
BanUserMessage,
|
||||||
VariableMessage,
|
VariableMessage, ErrorMessage,
|
||||||
} from "../Messages/generated/messages_pb";
|
} from "../Messages/generated/messages_pb";
|
||||||
|
|
||||||
import type { UserSimplePeerInterface } from "../WebRtc/SimplePeer";
|
import type { UserSimplePeerInterface } from "../WebRtc/SimplePeer";
|
||||||
@ -165,6 +165,9 @@ 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()) {
|
} else if (subMessage.hasVariablemessage()) {
|
||||||
event = EventMessage.SET_VARIABLE;
|
event = EventMessage.SET_VARIABLE;
|
||||||
payload = subMessage.getVariablemessage();
|
payload = subMessage.getVariablemessage();
|
||||||
|
@ -26,6 +26,11 @@ export class SharedVariablesManager {
|
|||||||
|
|
||||||
// Let's initialize default values
|
// Let's initialize default values
|
||||||
for (const [name, variableObject] of this.variableObjects.entries()) {
|
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);
|
this._variables.set(name, variableObject.defaultValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -35,7 +40,6 @@ export class SharedVariablesManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
roomConnection.onSetVariable((name, value) => {
|
roomConnection.onSetVariable((name, value) => {
|
||||||
console.log('Set Variable received from server');
|
|
||||||
this._variables.set(name, value);
|
this._variables.set(name, value);
|
||||||
|
|
||||||
// On server change, let's notify the iframes
|
// On server change, let's notify the iframes
|
||||||
|
@ -23,4 +23,11 @@ WA.onInit().then(() => {
|
|||||||
WA.state.saveVariable('config', {'foo': 'bar'}).catch(e => {
|
WA.state.saveVariable('config', {'foo': 'bar'}).catch(e => {
|
||||||
console.log('Successfully caught error because variable "config" is not writable: ', 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.');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
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
|
||||||
|
}
|
@ -142,6 +142,29 @@
|
|||||||
"width":0,
|
"width":0,
|
||||||
"x":88.8149900876127,
|
"x":88.8149900876127,
|
||||||
"y":147.75212636695
|
"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,
|
"opacity":1,
|
||||||
"type":"objectgroup",
|
"type":"objectgroup",
|
||||||
@ -150,7 +173,7 @@
|
|||||||
"y":0
|
"y":0
|
||||||
}],
|
}],
|
||||||
"nextlayerid":8,
|
"nextlayerid":8,
|
||||||
"nextobjectid":10,
|
"nextobjectid":11,
|
||||||
"orientation":"orthogonal",
|
"orientation":"orthogonal",
|
||||||
"properties":[
|
"properties":[
|
||||||
{
|
{
|
||||||
|
@ -113,6 +113,15 @@ message VariableMessage {
|
|||||||
string value = 2;
|
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;
|
||||||
@ -140,6 +149,7 @@ message SubMessage {
|
|||||||
ItemEventMessage itemEventMessage = 6;
|
ItemEventMessage itemEventMessage = 6;
|
||||||
EmoteEventMessage emoteEventMessage = 7;
|
EmoteEventMessage emoteEventMessage = 7;
|
||||||
VariableMessage variableMessage = 8;
|
VariableMessage variableMessage = 8;
|
||||||
|
ErrorMessage errorMessage = 9;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -365,6 +375,7 @@ message SubToPusherMessage {
|
|||||||
SendUserMessage sendUserMessage = 7;
|
SendUserMessage sendUserMessage = 7;
|
||||||
BanUserMessage banUserMessage = 8;
|
BanUserMessage banUserMessage = 8;
|
||||||
EmoteEventMessage emoteEventMessage = 9;
|
EmoteEventMessage emoteEventMessage = 9;
|
||||||
|
ErrorMessage errorMessage = 10;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -374,7 +385,8 @@ message BatchToPusherRoomMessage {
|
|||||||
|
|
||||||
message SubToPusherRoomMessage {
|
message SubToPusherRoomMessage {
|
||||||
oneof message {
|
oneof message {
|
||||||
VariableMessage variableMessage = 1;
|
VariableWithTagMessage variableMessage = 1;
|
||||||
|
ErrorMessage errorMessage = 2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@ import { apiClientRepository } from "../Services/ApiClientRepository";
|
|||||||
import {
|
import {
|
||||||
BatchToPusherMessage,
|
BatchToPusherMessage,
|
||||||
BatchToPusherRoomMessage,
|
BatchToPusherRoomMessage,
|
||||||
EmoteEventMessage,
|
EmoteEventMessage, ErrorMessage,
|
||||||
GroupLeftZoneMessage,
|
GroupLeftZoneMessage,
|
||||||
GroupUpdateZoneMessage,
|
GroupUpdateZoneMessage,
|
||||||
RoomMessage,
|
RoomMessage,
|
||||||
@ -15,7 +15,7 @@ import {
|
|||||||
UserJoinedZoneMessage,
|
UserJoinedZoneMessage,
|
||||||
UserLeftZoneMessage,
|
UserLeftZoneMessage,
|
||||||
UserMovedMessage,
|
UserMovedMessage,
|
||||||
VariableMessage,
|
VariableMessage, VariableWithTagMessage,
|
||||||
ZoneMessage,
|
ZoneMessage,
|
||||||
} from "../Messages/generated/messages_pb";
|
} from "../Messages/generated/messages_pb";
|
||||||
import Debug from "debug";
|
import Debug from "debug";
|
||||||
@ -38,7 +38,7 @@ export class PusherRoom {
|
|||||||
private backConnection!: ClientReadableStream<BatchToPusherRoomMessage>;
|
private backConnection!: ClientReadableStream<BatchToPusherRoomMessage>;
|
||||||
private isClosing: boolean = false;
|
private isClosing: boolean = false;
|
||||||
private listeners: Set<ExSocketInterface> = new Set<ExSocketInterface>();
|
private listeners: Set<ExSocketInterface> = new Set<ExSocketInterface>();
|
||||||
public readonly variables = new Map<string, string>();
|
//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 = [];
|
||||||
@ -90,15 +90,27 @@ export class PusherRoom {
|
|||||||
this.backConnection.on("data", (batch: BatchToPusherRoomMessage) => {
|
this.backConnection.on("data", (batch: BatchToPusherRoomMessage) => {
|
||||||
for (const message of batch.getPayloadList()) {
|
for (const message of batch.getPayloadList()) {
|
||||||
if (message.hasVariablemessage()) {
|
if (message.hasVariablemessage()) {
|
||||||
const variableMessage = message.getVariablemessage() as VariableMessage;
|
const variableMessage = message.getVariablemessage() as VariableWithTagMessage;
|
||||||
|
const readableBy = variableMessage.getReadableby();
|
||||||
|
|
||||||
// We need to store all variables to dispatch variables later to the listeners
|
// We need to store all variables to dispatch variables later to the listeners
|
||||||
this.variables.set(variableMessage.getName(), variableMessage.getValue());
|
//this.variables.set(variableMessage.getName(), variableMessage.getValue(), readableBy);
|
||||||
|
|
||||||
// Let's dispatch this variable to all the listeners
|
// Let's dispatch this variable to all the listeners
|
||||||
for (const listener of this.listeners) {
|
for (const listener of this.listeners) {
|
||||||
const subMessage = new SubMessage();
|
const subMessage = new SubMessage();
|
||||||
subMessage.setVariablemessage(variableMessage);
|
if (!readableBy || listener.tags.indexOf(readableBy) !== -1) {
|
||||||
|
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);
|
listener.emitInBatch(subMessage);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -14,7 +14,7 @@ import {
|
|||||||
UserMovedMessage,
|
UserMovedMessage,
|
||||||
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 +30,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 +218,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 +307,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,7 +30,7 @@ import {
|
|||||||
BanMessage,
|
BanMessage,
|
||||||
RefreshRoomMessage,
|
RefreshRoomMessage,
|
||||||
EmotePromptMessage,
|
EmotePromptMessage,
|
||||||
VariableMessage,
|
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";
|
||||||
@ -281,6 +281,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();
|
||||||
|
@ -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