Adding support for "readableBy" and "writableBy" in back

This means that we are now loading maps from server side.
This commit is contained in:
David Négrier 2021-07-19 10:16:43 +02:00
parent 3d76f76d3e
commit dbd5b80636
24 changed files with 768 additions and 132 deletions

View file

@ -11,39 +11,54 @@ import {
EmoteEventMessage,
JoinRoomMessage,
SubToPusherRoomMessage,
VariableMessage,
VariableMessage, VariableWithTagMessage,
} from "../Messages/generated/messages_pb";
import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils";
import { RoomSocket, ZoneSocket } from "src/RoomManager";
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 DisconnectCallback = (user: User, group: Group) => void;
export class GameRoom {
private readonly minDistance: number;
private readonly groupRadius: number;
// Users, sorted by ID
private readonly users: Map<number, User>;
private readonly usersByUuid: Map<string, User>;
private readonly groups: Set<Group>;
private readonly admins: Set<Admin>;
private readonly connectCallback: ConnectCallback;
private readonly disconnectCallback: DisconnectCallback;
private readonly users = new Map<number, User>();
private readonly usersByUuid = new Map<string, User>();
private readonly groups = new Set<Group>();
private readonly admins = new Set<Admin>();
private itemsState = new Map<number, unknown>();
public readonly variables = new Map<string, string>();
private readonly positionNotifier: PositionNotifier;
public readonly roomUrl: string;
private versionNumber: number = 1;
private nextUserId: number = 1;
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,
connectCallback: ConnectCallback,
disconnectCallback: DisconnectCallback,
@ -53,19 +68,12 @@ export class GameRoom {
onMoves: MovesCallback,
onLeaves: LeavesCallback,
onEmote: EmoteCallback
) {
this.roomUrl = roomUrl;
) : Promise<GameRoom> {
const mapDetails = await GameRoom.getMapDetails(roomUrl);
this.users = new Map<number, User>();
this.usersByUuid = new Map<string, User>();
this.admins = new Set<Admin>();
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);
const gameRoom = new GameRoom(roomUrl, mapDetails.mapUrl, connectCallback, disconnectCallback, minDistance, groupRadius, onEnters, onMoves, onLeaves, onEmote);
return gameRoom;
}
public getGroups(): Group[] {
@ -299,13 +307,19 @@ export class GameRoom {
return this.itemsState;
}
public setVariable(name: string, value: string): void {
this.variables.set(name, value);
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 VariableMessage();
const variableMessage = new VariableWithTagMessage();
variableMessage.setName(name);
variableMessage.setValue(value);
if (readableBy) {
variableMessage.setReadableby(readableBy);
}
const subMessage = new SubToPusherRoomMessage();
subMessage.setVariablemessage(variableMessage);
@ -356,4 +370,82 @@ export class GameRoom {
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 = 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);
}
}