2021-06-24 10:09:10 +02:00
import { PointInterface } from "./Websocket/PointInterface" ;
import { Group } from "./Group" ;
import { User , UserSocket } from "./User" ;
import { PositionInterface } from "_Model/PositionInterface" ;
import { EmoteCallback , EntersCallback , LeavesCallback , MovesCallback } from "_Model/Zone" ;
import { PositionNotifier } from "./PositionNotifier" ;
import { Movable } from "_Model/Movable" ;
2021-07-07 17:17:28 +02:00
import {
BatchToPusherMessage ,
BatchToPusherRoomMessage ,
EmoteEventMessage ,
2021-07-21 16:29:38 +02:00
ErrorMessage ,
2021-07-16 11:22:36 +02:00
JoinRoomMessage ,
SubToPusherRoomMessage ,
2021-07-19 15:57:50 +02:00
VariableMessage ,
VariableWithTagMessage ,
2021-07-07 17:17:28 +02:00
} from "../Messages/generated/messages_pb" ;
2021-06-24 10:09:10 +02:00
import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils" ;
2021-07-16 11:22:36 +02:00
import { RoomSocket , ZoneSocket } from "src/RoomManager" ;
2021-06-24 10:09:10 +02:00
import { Admin } from "../Model/Admin" ;
2021-07-19 15:57:50 +02:00
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" ;
2021-07-21 16:29:38 +02:00
import { emitErrorOnRoomSocket } from "../Services/MessageHelpers" ;
2020-04-07 10:08:04 +02:00
2020-09-29 16:01:22 +02:00
export type ConnectCallback = ( user : User , group : Group ) = > void ;
export type DisconnectCallback = ( user : User , group : Group ) = > void ;
2020-05-03 16:08:04 +02:00
2020-10-06 15:37:00 +02:00
export class GameRoom {
2020-04-07 10:08:04 +02:00
// Users, sorted by ID
2021-07-19 10:16:43 +02:00
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 > ( ) ;
2020-04-07 10:08:04 +02:00
2021-07-06 15:30:49 +02:00
private itemsState = new Map < number , unknown > ( ) ;
2020-07-27 22:36:07 +02:00
2020-09-15 16:21:41 +02:00
private readonly positionNotifier : PositionNotifier ;
2021-06-24 10:09:10 +02:00
private versionNumber : number = 1 ;
2020-11-13 18:00:22 +01:00
private nextUserId : number = 1 ;
2020-10-14 16:00:25 +02:00
2021-07-07 17:17:28 +02:00
private roomListeners : Set < RoomSocket > = new Set < RoomSocket > ( ) ;
2021-07-19 10:16:43 +02:00
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 (
2021-07-13 19:09:07 +02:00
roomUrl : string ,
2021-06-24 10:09:10 +02:00
connectCallback : ConnectCallback ,
disconnectCallback : DisconnectCallback ,
minDistance : number ,
groupRadius : number ,
onEnters : EntersCallback ,
onMoves : MovesCallback ,
onLeaves : LeavesCallback ,
onEmote : EmoteCallback
2021-07-19 15:57:50 +02:00
) : Promise < GameRoom > {
2021-07-19 10:16:43 +02:00
const mapDetails = await GameRoom . getMapDetails ( roomUrl ) ;
2021-07-19 15:57:50 +02:00
const gameRoom = new GameRoom (
roomUrl ,
mapDetails . mapUrl ,
connectCallback ,
disconnectCallback ,
minDistance ,
groupRadius ,
onEnters ,
onMoves ,
onLeaves ,
onEmote
) ;
2021-07-19 10:16:43 +02:00
return gameRoom ;
2020-04-28 23:23:50 +02:00
}
2020-04-07 10:08:04 +02:00
2020-05-13 23:11:10 +02:00
public getGroups ( ) : Group [ ] {
2020-06-29 22:10:23 +02:00
return Array . from ( this . groups . values ( ) ) ;
2020-05-13 23:11:10 +02:00
}
2020-09-18 13:57:38 +02:00
public getUsers ( ) : Map < number , User > {
2020-05-19 19:11:12 +02:00
return this . users ;
}
2021-06-24 10:09:10 +02:00
public getUserByUuid ( uuid : string ) : User | undefined {
2020-12-11 12:23:50 +01:00
return this . usersByUuid . get ( uuid ) ;
}
2021-06-24 10:09:10 +02:00
public getUserById ( id : number ) : User | undefined {
2021-06-01 15:35:25 +02:00
return this . users . get ( id ) ;
}
2021-07-27 14:42:32 +02:00
public getUsersByUuid ( uuid : string ) : User [ ] {
const userList : User [ ] = [ ] ;
for ( const user of this . users . values ( ) ) {
if ( user . uuid === uuid ) {
userList . push ( user ) ;
}
}
return userList ;
}
2021-06-24 10:09:10 +02:00
public join ( socket : UserSocket , joinRoomMessage : JoinRoomMessage ) : User {
2020-11-13 18:00:22 +01:00
const positionMessage = joinRoomMessage . getPositionmessage ( ) ;
if ( positionMessage === undefined ) {
2021-06-24 10:09:10 +02:00
throw new Error ( "Missing position message" ) ;
2020-11-13 18:00:22 +01:00
}
const position = ProtobufUtils . toPointInterface ( positionMessage ) ;
2021-06-24 10:09:10 +02:00
const user = new User (
this . nextUserId ,
2021-01-15 03:19:58 +01:00
joinRoomMessage . getUseruuid ( ) ,
joinRoomMessage . getIpaddress ( ) ,
position ,
false ,
this . positionNotifier ,
socket ,
joinRoomMessage . getTagList ( ) ,
2021-06-08 16:30:58 +02:00
joinRoomMessage . getVisitcardurl ( ) ,
2021-01-15 03:19:58 +01:00
joinRoomMessage . getName ( ) ,
2021-04-02 21:21:11 +02:00
ProtobufUtils . toCharacterLayerObjects ( joinRoomMessage . getCharacterlayerList ( ) ) ,
joinRoomMessage . getCompanion ( )
2021-01-15 03:19:58 +01:00
) ;
2020-11-13 18:00:22 +01:00
this . nextUserId ++ ;
this . users . set ( user . id , user ) ;
2020-12-11 12:23:50 +01:00
this . usersByUuid . set ( user . uuid , user ) ;
2020-09-25 15:25:06 +02:00
this . updateUserGroup ( user ) ;
2020-12-10 17:46:15 +01:00
// Notify admins
for ( const admin of this . admins ) {
2021-01-15 03:19:58 +01:00
admin . sendUserJoin ( user . uuid , user . name , user . IPAddress ) ;
2020-12-10 17:46:15 +01:00
}
2020-11-13 18:00:22 +01:00
return user ;
2020-04-07 10:08:04 +02:00
}
2021-06-24 10:09:10 +02:00
public leave ( user : User ) {
2020-11-13 18:00:22 +01:00
const userObj = this . users . get ( user . id ) ;
2020-05-14 23:19:48 +02:00
if ( userObj === undefined ) {
2021-06-24 10:09:10 +02:00
console . warn ( "User " , user . id , "does not belong to this game room! It should!" ) ;
2020-05-14 23:19:48 +02:00
}
2021-06-24 10:09:10 +02:00
if ( userObj !== undefined && typeof userObj . group !== "undefined" ) {
2020-05-14 23:19:48 +02:00
this . leaveGroup ( userObj ) ;
2020-04-29 23:18:42 +02:00
}
2020-11-13 18:00:22 +01:00
this . users . delete ( user . id ) ;
2020-12-11 12:23:50 +01:00
this . usersByUuid . delete ( user . uuid ) ;
2020-09-15 16:21:41 +02:00
if ( userObj !== undefined ) {
2020-09-28 18:52:54 +02:00
this . positionNotifier . leave ( userObj ) ;
2020-09-15 16:21:41 +02:00
}
2020-12-10 17:46:15 +01:00
// Notify admins
for ( const admin of this . admins ) {
2021-06-24 10:09:10 +02:00
admin . sendUserLeft ( user . uuid /*, user.name, user.IPAddress*/ ) ;
2020-12-10 17:46:15 +01:00
}
2020-04-29 01:40:32 +02:00
}
2020-06-29 19:14:54 +02:00
public isEmpty ( ) : boolean {
2020-12-10 17:46:15 +01:00
return this . users . size === 0 && this . admins . size === 0 ;
2020-06-29 19:14:54 +02:00
}
2021-06-24 10:09:10 +02:00
public updatePosition ( user : User , userPosition : PointInterface ) : void {
2020-09-25 15:25:06 +02:00
user . setPosition ( userPosition ) ;
2020-09-16 16:06:43 +02:00
2020-09-25 15:25:06 +02:00
this . updateUserGroup ( user ) ;
}
2020-09-15 16:21:41 +02:00
2020-09-25 15:25:06 +02:00
private updateUserGroup ( user : User ) : void {
2020-09-24 10:05:16 +02:00
user . group ? . updatePosition ( ) ;
2020-04-09 23:26:19 +02:00
2020-08-31 14:03:40 +02:00
if ( user . silent ) {
return ;
}
2020-09-16 16:06:43 +02:00
if ( user . group === undefined ) {
2020-04-09 23:26:19 +02:00
// If the user is not part of a group:
// should he join a group?
2020-10-22 16:15:30 +02:00
// If the user is moving, don't try to join
if ( user . getPosition ( ) . moving ) {
return ;
}
2021-06-24 10:09:10 +02:00
const closestItem : User | Group | null = this . searchClosestAvailableUserOrGroup ( user ) ;
2020-04-09 23:26:19 +02:00
2020-04-29 22:41:48 +02:00
if ( closestItem !== null ) {
if ( closestItem instanceof Group ) {
// Let's join the group!
closestItem . join ( user ) ;
} else {
2021-06-24 10:09:10 +02:00
const closestUser : User = closestItem ;
const group : Group = new Group (
2021-07-13 19:09:07 +02:00
this . roomUrl ,
2021-06-24 10:09:10 +02:00
[ user , closestUser ] ,
this . connectCallback ,
this . disconnectCallback ,
this . positionNotifier
) ;
2020-06-29 22:13:07 +02:00
this . groups . add ( group ) ;
2020-04-09 23:26:19 +02:00
}
}
2020-04-28 23:23:50 +02:00
} else {
// If the user is part of a group:
2020-04-29 23:12:55 +02:00
// should he leave the group?
2020-10-06 15:37:00 +02:00
const distance = GameRoom . computeDistanceBetweenPositions ( user . getPosition ( ) , user . group . getPosition ( ) ) ;
2020-05-03 16:56:19 +02:00
if ( distance > this . groupRadius ) {
2020-04-29 23:12:55 +02:00
this . leaveGroup ( user ) ;
}
}
}
2020-04-28 23:23:50 +02:00
2020-11-13 18:00:22 +01:00
setSilent ( user : User , silent : boolean ) {
2020-08-31 14:03:40 +02:00
if ( user . silent === silent ) {
return ;
}
user . silent = silent ;
if ( silent && user . group !== undefined ) {
this . leaveGroup ( user ) ;
}
if ( ! silent ) {
// If we are back to life, let's trigger a position update to see if we can join some group.
2020-11-13 18:00:22 +01:00
this . updatePosition ( user , user . getPosition ( ) ) ;
2020-08-31 14:03:40 +02:00
}
}
2020-04-29 23:12:55 +02:00
/ * *
* Makes a user leave a group and closes and destroy the group if the group contains only one remaining person .
*
* @param user
* /
2020-09-16 16:06:43 +02:00
private leaveGroup ( user : User ) : void {
2020-06-09 15:54:54 +02:00
const group = user . group ;
2020-09-21 11:24:03 +02:00
if ( group === undefined ) {
2020-04-29 23:12:55 +02:00
throw new Error ( "The user is part of no group" ) ;
}
group . leave ( user ) ;
if ( group . isEmpty ( ) ) {
2020-09-16 16:06:43 +02:00
this . positionNotifier . leave ( group ) ;
2020-04-29 23:12:55 +02:00
group . destroy ( ) ;
2020-06-29 22:13:07 +02:00
if ( ! this . groups . has ( group ) ) {
2021-06-24 10:09:10 +02:00
throw new Error (
"Could not find group " + group . getId ( ) + " referenced by user " + user . id + " in World."
) ;
2020-04-29 23:12:55 +02:00
}
2020-06-29 22:13:07 +02:00
this . groups . delete ( group ) ;
2020-10-30 15:23:50 +01:00
//todo: is the group garbage collected?
2020-05-08 00:35:36 +02:00
} else {
2020-09-25 13:48:02 +02:00
group . updatePosition ( ) ;
//this.positionNotifier.updatePosition(group, group.getPosition(), oldPosition);
2020-04-09 23:26:19 +02:00
}
}
/ * *
* Looks for the closest user that is :
2020-05-03 16:56:19 +02:00
* - close enough ( distance <= minDistance )
* - not in a group
2020-08-31 14:03:40 +02:00
* - not silent
2020-05-03 16:56:19 +02:00
* OR
* - close enough to a group ( distance <= groupRadius )
2020-04-09 23:26:19 +02:00
* /
2021-06-24 10:09:10 +02:00
private searchClosestAvailableUserOrGroup ( user : User ) : User | Group | null {
2020-05-03 16:56:19 +02:00
let minimumDistanceFound : number = Math . max ( this . minDistance , this . groupRadius ) ;
2020-09-16 16:06:43 +02:00
let matchingItem : User | Group | null = null ;
2020-05-03 16:56:19 +02:00
this . users . forEach ( ( currentUser , userId ) = > {
2020-04-29 22:41:48 +02:00
// Let's only check users that are not part of a group
2021-06-24 10:09:10 +02:00
if ( typeof currentUser . group !== "undefined" ) {
2020-04-29 22:41:48 +02:00
return ;
}
2021-06-24 10:09:10 +02:00
if ( currentUser === user ) {
2020-04-09 23:26:19 +02:00
return ;
}
2020-08-31 14:03:40 +02:00
if ( currentUser . silent ) {
return ;
}
2020-04-09 23:26:19 +02:00
2020-10-06 15:37:00 +02:00
const distance = GameRoom . computeDistance ( user , currentUser ) ; // compute distance between peers.
2020-04-28 23:23:50 +02:00
2021-06-24 10:09:10 +02:00
if ( distance <= minimumDistanceFound && distance <= this . minDistance ) {
2020-04-29 22:41:48 +02:00
minimumDistanceFound = distance ;
matchingItem = currentUser ;
}
} ) ;
2020-05-03 16:56:19 +02:00
this . groups . forEach ( ( group : Group ) = > {
2020-04-29 22:41:48 +02:00
if ( group . isFull ( ) ) {
return ;
2020-04-08 20:40:44 +02:00
}
2020-10-06 15:37:00 +02:00
const distance = GameRoom . computeDistanceBetweenPositions ( user . getPosition ( ) , group . getPosition ( ) ) ;
2021-06-24 10:09:10 +02:00
if ( distance <= minimumDistanceFound && distance <= this . groupRadius ) {
2020-04-29 22:41:48 +02:00
minimumDistanceFound = distance ;
matchingItem = group ;
}
} ) ;
2020-04-08 20:40:44 +02:00
2020-04-29 22:41:48 +02:00
return matchingItem ;
2020-04-08 20:40:44 +02:00
}
2021-06-24 10:09:10 +02:00
public static computeDistance ( user1 : User , user2 : User ) : number {
2020-09-25 15:25:06 +02:00
const user1Position = user1 . getPosition ( ) ;
const user2Position = user2 . getPosition ( ) ;
2021-06-24 10:09:10 +02:00
return Math . sqrt (
Math . pow ( user2Position . x - user1Position . x , 2 ) + Math . pow ( user2Position . y - user1Position . y , 2 )
) ;
2020-04-08 20:40:44 +02:00
}
2020-04-07 10:08:04 +02:00
2021-06-24 10:09:10 +02:00
public static computeDistanceBetweenPositions ( position1 : PositionInterface , position2 : PositionInterface ) : number {
2020-04-29 22:41:48 +02:00
return Math . sqrt ( Math . pow ( position2 . x - position1 . x , 2 ) + Math . pow ( position2 . y - position1 . y , 2 ) ) ;
}
2020-07-27 22:36:07 +02:00
public setItemState ( itemId : number , state : unknown ) {
this . itemsState . set ( itemId , state ) ;
}
public getItemsState ( ) : Map < number , unknown > {
return this . itemsState ;
}
2021-07-19 10:16:43 +02:00
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 ) ;
2021-07-07 17:17:28 +02:00
2021-07-23 11:50:03 +02:00
// If the variable was not changed, let's not dispatch anything.
if ( readableBy === false ) {
return ;
}
2021-07-07 17:17:28 +02:00
// TODO: should we batch those every 100ms?
2021-07-19 10:16:43 +02:00
const variableMessage = new VariableWithTagMessage ( ) ;
2021-07-07 17:17:28 +02:00
variableMessage . setName ( name ) ;
variableMessage . setValue ( value ) ;
2021-07-19 10:16:43 +02:00
if ( readableBy ) {
variableMessage . setReadableby ( readableBy ) ;
}
2021-07-07 17:17:28 +02:00
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 ) ;
}
2021-07-06 15:30:49 +02:00
}
2020-11-13 18:00:22 +01:00
public addZoneListener ( call : ZoneSocket , x : number , y : number ) : Set < Movable > {
return this . positionNotifier . addZoneListener ( call , x , y ) ;
2020-09-15 16:21:41 +02:00
}
2020-10-14 16:00:25 +02:00
2020-11-13 18:00:22 +01:00
public removeZoneListener ( call : ZoneSocket , x : number , y : number ) : void {
return this . positionNotifier . removeZoneListener ( call , x , y ) ;
2020-10-14 16:00:25 +02:00
}
2020-12-10 17:46:15 +01:00
public adminJoin ( admin : Admin ) : void {
this . admins . add ( admin ) ;
// Let's send all connected users
for ( const user of this . users . values ( ) ) {
2021-01-15 03:19:58 +01:00
admin . sendUserJoin ( user . uuid , user . name , user . IPAddress ) ;
2020-12-10 17:46:15 +01:00
}
}
public adminLeave ( admin : Admin ) : void {
this . admins . delete ( admin ) ;
}
2021-06-24 10:09:10 +02:00
2021-04-01 16:43:12 +02:00
public incrementVersion ( ) : number {
2021-06-24 10:09:10 +02:00
this . versionNumber ++ ;
2021-04-01 16:43:12 +02:00
return this . versionNumber ;
}
2021-03-31 11:21:06 +02:00
public emitEmoteEvent ( user : User , emoteEventMessage : EmoteEventMessage ) {
this . positionNotifier . emitEmoteEvent ( user , emoteEventMessage ) ;
}
2021-07-07 17:17:28 +02:00
public addRoomListener ( socket : RoomSocket ) {
this . roomListeners . add ( socket ) ;
}
public removeRoomListener ( socket : RoomSocket ) {
this . roomListeners . delete ( socket ) ;
}
2021-07-19 10:16:43 +02:00
/ * *
* 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 : / _ / i n s t a n c e / m a p _ u r l )
* /
private static async getMapDetails ( roomUrl : string ) : Promise < MapDetailsData > {
if ( ! ADMIN_API_URL ) {
const roomUrlObj = new URL ( roomUrl ) ;
const match = /\/_\/[^/]+\/(.+)/ . exec ( roomUrlObj . pathname ) ;
if ( ! match ) {
2021-07-19 15:57:50 +02:00
console . error ( "Unexpected room URL" , roomUrl ) ;
2021-07-19 10:16:43 +02:00
throw new Error ( 'Unexpected room URL "' + roomUrl + '"' ) ;
}
const mapUrl = roomUrlObj . protocol + "//" + match [ 1 ] ;
return {
mapUrl ,
policy_type : 1 ,
textures : [ ] ,
tags : [ ] ,
2021-07-19 15:57:50 +02:00
} ;
2021-07-19 10:16:43 +02:00
}
const result = await adminApi . fetchMapDetails ( roomUrl ) ;
if ( ! isMapDetailsData ( result ) ) {
2021-07-19 15:57:50 +02:00
console . error ( "Unexpected room details received from server" , result ) ;
throw new Error ( "Unexpected room details received from server" ) ;
2021-07-19 10:16:43 +02:00
}
return result ;
}
2021-07-19 15:57:50 +02:00
private mapPromise : Promise < ITiledMap > | undefined ;
2021-07-19 10:16:43 +02:00
/ * *
* 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 ;
}
2021-07-19 15:57:50 +02:00
private variableManagerPromise : Promise < VariablesManager > | undefined ;
2021-07-19 10:16:43 +02:00
private getVariableManager ( ) : Promise < VariablesManager > {
if ( ! this . variableManagerPromise ) {
2021-07-21 18:21:12 +02:00
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 {
2021-08-03 10:08:53 +02:00
// An error occurred while loading the map
// Right now, let's bypass the error. In the future, we should make sure the user is aware of that
// and that he/she will act on it to fix the problem.
// 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 ,
"Your map does not seem accessible from the WorkAdventure servers. Is it behind a firewall or a proxy? Your map should be accessible from the WorkAdventure servers. 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 ( ) ;
2021-07-21 18:21:12 +02:00
}
} ) ;
2021-07-19 10:16:43 +02:00
}
return this . variableManagerPromise ;
}
public async getVariablesForTags ( tags : string [ ] ) : Promise < Map < string , string > > {
const variablesManager = await this . getVariableManager ( ) ;
return variablesManager . getVariablesForTags ( tags ) ;
}
2020-04-28 23:23:50 +02:00
}