2021-07-19 10:16:43 +02:00
|
|
|
/**
|
|
|
|
* Handles variables shared between the scripting API and the server.
|
|
|
|
*/
|
2021-09-02 17:29:29 +02:00
|
|
|
import {
|
|
|
|
ITiledMap,
|
|
|
|
ITiledMapLayer,
|
|
|
|
ITiledMapObject,
|
|
|
|
ITiledMapObjectLayer,
|
|
|
|
} from "@workadventure/tiled-map-type-guard/dist";
|
2021-07-19 15:57:50 +02:00
|
|
|
import { User } from "_Model/User";
|
|
|
|
import { variablesRepository } from "./Repository/VariablesRepository";
|
|
|
|
import { redisClient } from "./RedisClient";
|
2021-07-19 10:16:43 +02:00
|
|
|
|
|
|
|
interface Variable {
|
2021-07-19 15:57:50 +02:00
|
|
|
defaultValue?: string;
|
|
|
|
persist?: boolean;
|
|
|
|
readableBy?: string;
|
|
|
|
writableBy?: string;
|
2021-07-19 10:16:43 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
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.
|
|
|
|
*/
|
2021-07-19 15:57:50 +02:00
|
|
|
constructor(private roomUrl: string, private map: ITiledMap | null) {
|
2021-07-19 10:16:43 +02:00
|
|
|
// 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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-07-19 15:57:50 +02:00
|
|
|
/**
|
|
|
|
* Let's load data from the Redis backend.
|
|
|
|
*/
|
2021-07-21 18:21:12 +02:00
|
|
|
public async init(): Promise<VariablesManager> {
|
2021-07-19 15:57:50 +02:00
|
|
|
if (!this.shouldPersist()) {
|
2021-07-21 18:21:12 +02:00
|
|
|
return this;
|
2021-07-19 15:57:50 +02:00
|
|
|
}
|
|
|
|
const variables = await variablesRepository.loadVariables(this.roomUrl);
|
|
|
|
for (const key in variables) {
|
2021-07-23 12:19:47 +02:00
|
|
|
// Let's only set variables if they are in the map (if the map has changed, maybe stored variables do not exist anymore)
|
|
|
|
if (this.variableObjects) {
|
|
|
|
const variableObject = this.variableObjects.get(key);
|
|
|
|
if (variableObject === undefined) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
if (!variableObject.persist) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-07-19 15:57:50 +02:00
|
|
|
this._variables.set(key, variables[key]);
|
|
|
|
}
|
2021-07-21 18:21:12 +02:00
|
|
|
return this;
|
2021-07-19 15:57:50 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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");
|
|
|
|
}
|
|
|
|
|
2021-07-19 10:16:43 +02:00
|
|
|
private static findVariablesInMap(map: ITiledMap): Map<string, Variable> {
|
|
|
|
const objects = new Map<string, Variable>();
|
|
|
|
for (const layer of map.layers) {
|
2021-09-02 17:29:29 +02:00
|
|
|
this.recursiveFindVariablesInLayer(layer, objects);
|
|
|
|
}
|
|
|
|
return objects;
|
|
|
|
}
|
2021-07-19 10:16:43 +02:00
|
|
|
|
2021-09-02 17:29:29 +02:00
|
|
|
private static recursiveFindVariablesInLayer(layer: ITiledMapLayer, objects: Map<string, Variable>): void {
|
|
|
|
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.'
|
|
|
|
);
|
|
|
|
continue;
|
2021-07-19 10:16:43 +02:00
|
|
|
}
|
2021-09-02 17:29:29 +02:00
|
|
|
|
|
|
|
// We store a copy of the object (to make it immutable)
|
|
|
|
objects.set(object.name, this.iTiledObjectToVariable(object));
|
2021-07-19 10:16:43 +02:00
|
|
|
}
|
|
|
|
}
|
2021-09-02 17:29:29 +02:00
|
|
|
} else if (layer.type === "group") {
|
|
|
|
for (const innerLayer of layer.layers) {
|
|
|
|
this.recursiveFindVariablesInLayer(innerLayer, objects);
|
|
|
|
}
|
2021-07-19 10:16:43 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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) {
|
2021-07-19 15:57:50 +02:00
|
|
|
case "default":
|
2021-07-19 10:16:43 +02:00
|
|
|
variable.defaultValue = JSON.stringify(value);
|
|
|
|
break;
|
2021-07-19 15:57:50 +02:00
|
|
|
case "persist":
|
|
|
|
if (typeof value !== "boolean") {
|
2021-07-19 10:16:43 +02:00
|
|
|
throw new Error('The persist property of variable "' + object.name + '" must be a boolean');
|
|
|
|
}
|
|
|
|
variable.persist = value;
|
|
|
|
break;
|
2021-07-19 15:57:50 +02:00
|
|
|
case "writableBy":
|
|
|
|
if (typeof value !== "string") {
|
|
|
|
throw new Error(
|
|
|
|
'The writableBy property of variable "' + object.name + '" must be a string'
|
|
|
|
);
|
2021-07-19 10:16:43 +02:00
|
|
|
}
|
|
|
|
if (value) {
|
|
|
|
variable.writableBy = value;
|
|
|
|
}
|
|
|
|
break;
|
2021-07-19 15:57:50 +02:00
|
|
|
case "readableBy":
|
|
|
|
if (typeof value !== "string") {
|
|
|
|
throw new Error(
|
|
|
|
'The readableBy property of variable "' + object.name + '" must be a string'
|
|
|
|
);
|
2021-07-19 10:16:43 +02:00
|
|
|
}
|
|
|
|
if (value) {
|
|
|
|
variable.readableBy = value;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return variable;
|
|
|
|
}
|
|
|
|
|
2021-07-23 11:50:03 +02:00
|
|
|
/**
|
|
|
|
* Sets the variable.
|
|
|
|
*
|
|
|
|
* Returns who is allowed to read the variable (the readableby property) or "undefined" if anyone can read it.
|
|
|
|
* Also, returns "false" if the variable was not modified (because we set it to the value it already has)
|
|
|
|
*
|
|
|
|
* @param name
|
|
|
|
* @param value
|
|
|
|
* @param user
|
|
|
|
*/
|
|
|
|
setVariable(name: string, value: string, user: User): string | undefined | false {
|
2021-07-19 10:16:43 +02:00
|
|
|
let readableBy: string | undefined;
|
2021-07-23 12:19:47 +02:00
|
|
|
let variableObject: Variable | undefined;
|
2021-07-19 10:16:43 +02:00
|
|
|
if (this.variableObjects) {
|
2021-07-23 12:19:47 +02:00
|
|
|
variableObject = this.variableObjects.get(name);
|
2021-07-19 10:16:43 +02:00
|
|
|
if (variableObject === undefined) {
|
|
|
|
throw new Error('Trying to set a variable "' + name + '" that is not defined as an object in the map.');
|
|
|
|
}
|
|
|
|
|
2021-07-19 15:57:50 +02:00
|
|
|
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(", ") +
|
|
|
|
"."
|
|
|
|
);
|
2021-07-19 10:16:43 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
readableBy = variableObject.readableBy;
|
|
|
|
}
|
|
|
|
|
2021-07-23 11:50:03 +02:00
|
|
|
// If the value is not modified, return false
|
|
|
|
if (this._variables.get(name) === value) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2021-07-19 10:16:43 +02:00
|
|
|
this._variables.set(name, value);
|
2021-07-23 12:19:47 +02:00
|
|
|
|
|
|
|
if (variableObject !== undefined && variableObject.persist) {
|
|
|
|
variablesRepository
|
|
|
|
.saveVariable(this.roomUrl, name, value)
|
|
|
|
.catch((e) => console.error("Error while saving variable in Redis:", e));
|
|
|
|
}
|
|
|
|
|
2021-07-19 10:16:43 +02:00
|
|
|
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) {
|
2021-07-19 15:57:50 +02:00
|
|
|
throw new Error('Unexpected variable "' + key + '" found has no associated variableObject.');
|
2021-07-19 10:16:43 +02:00
|
|
|
}
|
2021-07-19 15:57:50 +02:00
|
|
|
if (!variableObject.readableBy || tags.includes(variableObject.readableBy)) {
|
2021-07-19 10:16:43 +02:00
|
|
|
readableVariables.set(key, value);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return readableVariables;
|
|
|
|
}
|
|
|
|
}
|