diff --git a/src/components/map/layer/MapLayer.vue b/src/components/map/layer/MapLayer.vue index d94e149..d1e227d 100644 --- a/src/components/map/layer/MapLayer.vue +++ b/src/components/map/layer/MapLayer.vue @@ -20,6 +20,8 @@ import {Map} from 'leaflet'; import {useStore} from "@/store"; import {DynmapTileLayer} from "@/leaflet/tileLayer/DynmapTileLayer"; import LiveAtlasMapDefinition from "@/model/LiveAtlasMapDefinition"; +import {LiveAtlasTileLayer} from "@/leaflet/tileLayer/LiveAtlasTileLayer"; +import {Pl3xmapTileLayer} from "@/leaflet/tileLayer/Pl3xmapTileLayer"; export default defineComponent({ props: { @@ -39,11 +41,21 @@ export default defineComponent({ setup(props) { const store = useStore(), + active = computed(() => props.map === store.state.currentMap); + + let layer: LiveAtlasTileLayer; + + if(store.state.currentServer?.type === 'dynmap') { layer = new DynmapTileLayer({ errorTileUrl: 'images/blank.png', mapSettings: Object.freeze(JSON.parse(JSON.stringify(props.map))), - }), - active = computed(() => props.map === store.state.currentMap); + }); + } else { + layer = new Pl3xmapTileLayer({ + errorTileUrl: 'images/blank.png', + mapSettings: Object.freeze(JSON.parse(JSON.stringify(props.map))) + }); + } const enableLayer = () => { props.leaflet.addLayer(layer); diff --git a/src/index.d.ts b/src/index.d.ts index 23446d5..d547694 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -62,8 +62,9 @@ interface LiveAtlasGlobalConfig { interface LiveAtlasServerDefinition { id: string; label?: string; - type: 'dynmap' - dynmap: DynmapUrlConfig; + type: 'dynmap' | 'pl3xmap'; + dynmap?: DynmapUrlConfig; + pl3xmap?: string; } // Messages defined directly in LiveAtlas and used for all servers diff --git a/src/leaflet/tileLayer/DynmapTileLayer.ts b/src/leaflet/tileLayer/DynmapTileLayer.ts index 06c49d6..e1a152b 100644 --- a/src/leaflet/tileLayer/DynmapTileLayer.ts +++ b/src/leaflet/tileLayer/DynmapTileLayer.ts @@ -17,21 +17,15 @@ * limitations under the License. */ -import {Coords, DoneCallback, DomUtil, TileLayerOptions, TileLayer, Util} from 'leaflet'; +import {Coords, DoneCallback, DomUtil} from 'leaflet'; import {useStore} from "@/store"; import {Coordinate} from "@/index"; -import LiveAtlasMapDefinition from "@/model/LiveAtlasMapDefinition"; +import {LiveAtlasTileLayerOptions, LiveAtlasTileLayer} from "@/leaflet/tileLayer/LiveAtlasTileLayer"; import {computed, watch} from "@vue/runtime-core"; import {ComputedRef} from "@vue/reactivity"; import {WatchStopHandle} from "vue"; import {ActionTypes} from "@/store/action-types"; -export interface DynmapTileLayerOptions extends TileLayerOptions { - mapSettings: LiveAtlasMapDefinition; - errorTileUrl: string; - night?: boolean; -} - export interface DynmapTile { active?: boolean; coords: Coords; @@ -61,8 +55,7 @@ export interface TileInfo { const store = useStore(); // noinspection JSUnusedGlobalSymbols -export class DynmapTileLayer extends TileLayer { - private readonly _mapSettings: LiveAtlasMapDefinition; +export class DynmapTileLayer extends LiveAtlasTileLayer { private readonly _cachedTileUrls: Map = Object.seal(new Map()); private readonly _namedTiles: Map = Object.seal(new Map()); private readonly _loadQueue: DynmapTileElement[] = []; @@ -79,22 +72,10 @@ export class DynmapTileLayer extends TileLayer { // @ts-ignore declare options: DynmapTileLayerOptions; - constructor(options: DynmapTileLayerOptions) { + constructor(options: LiveAtlasTileLayerOptions) { super('', options); this._mapSettings = options.mapSettings; - options.maxZoom = this._mapSettings.nativeZoomLevels + this._mapSettings.extraZoomLevels; - options.maxNativeZoom = this._mapSettings.nativeZoomLevels; - options.zoomReverse = true; - options.tileSize = 128; - options.minZoom = 0; - - Util.setOptions(this, options); - - if (options.mapSettings === null) { - throw new TypeError("mapSettings missing"); - } - this._tileTemplate = DomUtil.create('img', 'leaflet-tile') as DynmapTileElement; this._tileTemplate.style.width = this._tileTemplate.style.height = this.options.tileSize + 'px'; this._tileTemplate.alt = ''; diff --git a/src/leaflet/tileLayer/LiveAtlasTileLayer.ts b/src/leaflet/tileLayer/LiveAtlasTileLayer.ts new file mode 100644 index 0000000..429ae58 --- /dev/null +++ b/src/leaflet/tileLayer/LiveAtlasTileLayer.ts @@ -0,0 +1,46 @@ +/* + * Copyright 2021 James Lyne + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {TileLayer, TileLayerOptions, Util} from 'leaflet'; +import LiveAtlasMapDefinition from "@/model/LiveAtlasMapDefinition"; + +export interface LiveAtlasTileLayerOptions extends TileLayerOptions { + mapSettings: LiveAtlasMapDefinition; + errorTileUrl: string; +} + +// noinspection JSUnusedGlobalSymbols +export abstract class LiveAtlasTileLayer extends TileLayer { + protected _mapSettings: LiveAtlasMapDefinition; + declare options: LiveAtlasTileLayerOptions; + + protected constructor(url: string, options: LiveAtlasTileLayerOptions) { + super(url, options); + + this._mapSettings = options.mapSettings; + options.maxZoom = this._mapSettings.nativeZoomLevels + this._mapSettings.extraZoomLevels; + options.maxNativeZoom = this._mapSettings.nativeZoomLevels; + options.zoomReverse = true; + options.tileSize = 128; + options.minZoom = 0; + + Util.setOptions(this, options); + + if (options.mapSettings === null) { + throw new TypeError("mapSettings missing"); + } + } +} diff --git a/src/leaflet/tileLayer/Pl3xmapTileLayer.ts b/src/leaflet/tileLayer/Pl3xmapTileLayer.ts new file mode 100644 index 0000000..9deff9c --- /dev/null +++ b/src/leaflet/tileLayer/Pl3xmapTileLayer.ts @@ -0,0 +1,34 @@ +/* + * Copyright 2021 James Lyne + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {LiveAtlasTileLayer, LiveAtlasTileLayerOptions} from "@/leaflet/tileLayer/LiveAtlasTileLayer"; +import {useStore} from "@/store"; +import {Util} from "leaflet"; + +// noinspection JSUnusedGlobalSymbols +export class Pl3xmapTileLayer extends LiveAtlasTileLayer { + constructor(options: LiveAtlasTileLayerOptions) { + const worldName = options.mapSettings.world.name, + baseUrl = useStore().state.currentMapProvider!.getTilesUrl(); + + super(`${baseUrl}${worldName}/{z}/{x}_{y}.png`, options); + + options.tileSize = 512; + options.zoomReverse = false; + + Util.setOptions(this, options); + } +} diff --git a/src/providers/Pl3xmapMapProvider.ts b/src/providers/Pl3xmapMapProvider.ts new file mode 100644 index 0000000..ceba348 --- /dev/null +++ b/src/providers/Pl3xmapMapProvider.ts @@ -0,0 +1,520 @@ +/* + * Copyright 2021 James Lyne + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + HeadQueueEntry, LiveAtlasArea, LiveAtlasCircle, LiveAtlasComponentConfig, + LiveAtlasDimension, LiveAtlasLine, LiveAtlasMarker, + LiveAtlasMarkerSet, LiveAtlasPartialComponentConfig, + LiveAtlasPlayer, LiveAtlasServerConfig, LiveAtlasServerDefinition, + LiveAtlasServerMessageConfig, + LiveAtlasWorldDefinition +} from "@/index"; +import LiveAtlasMapDefinition from "@/model/LiveAtlasMapDefinition"; +import {MutationTypes} from "@/store/mutation-types"; +import MapProvider from "@/providers/MapProvider"; +import {ActionTypes} from "@/store/action-types"; +import {titleColoursRegex} from "@/util"; + +export default class Pl3xmapMapProvider extends MapProvider { + private configurationAbort?: AbortController = undefined; + private markersAbort?: AbortController = undefined; + private playersAbort?: AbortController = undefined; + + private updatesEnabled = false; + private updateTimeout: number = 0; + private updateTimestamp: Date = new Date(); + private updateInterval: number = 3000; + private worldSettings: Map = new Map(); + + constructor(config: LiveAtlasServerDefinition) { + super(config); + } + + private static buildServerConfig(response: any): LiveAtlasServerConfig { + return { + title: (response.ui?.title || 'Pl3xmap').replace(titleColoursRegex, ''), + expandUI: response.ui?.sidebar?.pinned === 'pinned', + + //Not used by pl3xmap + defaultZoom: 1, + defaultMap: undefined, + defaultWorld: undefined, + followMap: undefined, + followZoom: undefined, + }; + } + + private static buildMessagesConfig(response: any): LiveAtlasServerMessageConfig { + return { + worldsHeading: response.ui?.sidebar?.world_list_label || '', + playersHeading: response.ui?.sidebar?.player_list_label || '', + + //Not used by pl3xmap + chatPlayerJoin: '', + chatPlayerQuit: '', + chatAnonymousJoin: '', + chatAnonymousQuit: '', + chatErrorNotAllowed: '', + chatErrorRequiresLogin: '', + chatErrorCooldown: '', + } + } + + private buildWorlds(serverResponse: any, worldResponses: any[]): Array { + const worlds: Array = []; + + (serverResponse.worlds || []).filter((w: any) => w && !!w.name).forEach((world: any, index: number) => { + const worldResponse = worldResponses[index], + worldConfig: {components: LiveAtlasPartialComponentConfig } = { + components: {}, + }; + + if(worldResponse.player_tracker?.enabled) { + worldConfig.components.playerMarkers = { + grayHiddenPlayers: true, + hideByDefault: !!worldResponse.player_tracker?.default_hidden, + layerName: worldResponse.player_tracker?.label || '', + layerPriority: worldResponse.player_tracker?.priority, + showBodies: false, + showSkinFaces: true, + showHealth: !!worldResponse.player_tracker?.nameplates?.show_health, + smallFaces: true, + } + } + + this.worldSettings.set(world.name, worldConfig); + + if(!worldResponse) { + console.warn(`World ${world.name} has no matching world config. Ignoring.`); + return; + } + + let dimension: LiveAtlasDimension = 'overworld'; + + if(world.type === 'nether') { + dimension = 'nether'; + } else if(world.type === 'the_end') { + dimension = 'nether'; + } + + const maps: Map = new Map(); + + maps.set('flat', Object.freeze(new LiveAtlasMapDefinition({ + world: world, + + background: 'transparent', + backgroundDay: 'transparent', + backgroundNight: 'transparent', + icon: undefined, + imageFormat: 'png', + name: 'flat', + displayName: 'Flat', + + nativeZoomLevels: worldResponse.zoom.max || 1, + extraZoomLevels: worldResponse.zoom.extra || 0, + }))); + + worlds.push({ + name: world.name || '(Unnamed world)', + displayName: world.display_name || world.name, + dimension, + protected: false, + seaLevel: 0, + height: 256, + center: {x: worldResponse.spawn.x, y: 0, z: worldResponse.spawn.z}, + maps, + }); + }); + + return Array.from(worlds.values()); + } + + private static buildComponents(response: any): LiveAtlasComponentConfig { + const components: LiveAtlasComponentConfig = { + markers: { + showLabels: false, + }, + coordinatesControl: undefined, + linkControl: !!response.ui?.link?.enabled, + layerControl: !!response.ui?.coordinates?.enabled, + + //Configured per-world + playerMarkers: undefined, + + //Not used by pl3xmap + chatBox: undefined, + chatBalloons: false, + clockControl: undefined, + logoControls: [], + login: false, + }; + + if(response.ui?.coordinates?.enabled) { + //Try to remove {x}/{z} placeholders are we aren't using them + const label = (response.ui?.coordinates?.html || "Location: ").replace(/{x}.*{z}/gi, '').trim(), + labelPlain = new DOMParser().parseFromString(label, 'text/html').body.textContent || ""; + + components.coordinatesControl = { + showY: false, + label: labelPlain, + showRegion: false, + showChunk: false, + } + } + + return components; + } + + private async getMarkerSets(world: LiveAtlasWorldDefinition): Promise> { + const url = `${this.config.pl3xmap}tiles/${world.name}/markers.json`; + + if(this.markersAbort) { + this.markersAbort.abort(); + } + + this.markersAbort = new AbortController(); + + const response = await Pl3xmapMapProvider.fetchJSON(url, this.markersAbort.signal); + const sets: Map = new Map(); + + if(!Array.isArray(response)) { + return sets; + } + + response.forEach(set => { + if(!set || !set.id) { + console.warn('Ignoring marker set without id'); + return; + } + + const id = set.id; + + const markers: Map = new Map(), + circles: Map = new Map(), + areas: Map = new Map(), + lines: Map = new Map(); + + (set.markers || []).forEach((marker: any) => { + switch(marker.type) { + case 'icon': + markers.set(`marker-${markers.size}`, Pl3xmapMapProvider.buildMarker(marker)); + break; + + case 'polyline': + lines.set(`line-${lines.size}`, Pl3xmapMapProvider.buildLine(marker)); + break; + + case 'rectangle': + areas.set(`area-${areas.size}`, Pl3xmapMapProvider.buildRectangle(marker)); + break; + + case 'polygon': + areas.set(`area-${areas.size}`, Pl3xmapMapProvider.buildArea(marker)); + break; + + case 'circle': + case 'ellipse': + circles.set(`circle-${circles.size}`, Pl3xmapMapProvider.buildCircle(marker)); + break; + + default: + console.warn('Marker type ' + marker.type + ' not supported'); + } + }); + + + const e = { + id, + label: set.name || "Unnamed set", + hidden: set.hide || false, + priority: set.order || 0, + showLabels: false, + markers, + circles, + areas, + lines, + }; + + sets.set(id, e); + }); + + return sets; + } + + private static buildMarker(marker: any): LiveAtlasMarker { + return { + location: { + x: marker.point?.x || 0, + y: 0, + z: marker.point?.z || 0, + }, + dimensions: marker.size ? [marker.size.x || 16, marker.size.z || 16] : [16, 16], + icon: marker.icon || "default", + + label: (marker.tooltip || '').trim(), + isLabelHTML: true + }; + } + + private static buildRectangle(area: any): LiveAtlasArea { + return Object.seal({ + style: { + stroke: typeof area.stroke !== 'undefined' ? !!area.stroke : true, + color: area.color || '#3388ff', + weight: area.weight || 3, + opacity: typeof area.opacity !== 'undefined' ? area.opacity : 1, + fill: typeof area.stroke !== 'undefined' ? !!area.stroke : true, + fillColor: area.fillColor || area.color || '#3388ff', + fillOpacity: area.fillOpacity || 0.2, + fillRule: area.fillRule, + }, + points: [ + area.points[0], + {x: area.points[0].x, z: area.points[1].z}, + area.points[1], + {x: area.points[1].x, z: area.points[0].z}, + ], + outline: false, + + tooltipContent: area.tooltip, + popupContent: area.popup, + isPopupHTML: true, + }); + } + + private static buildArea(area: any): LiveAtlasArea { + return Object.seal({ + style: { + stroke: typeof area.stroke !== 'undefined' ? !!area.stroke : true, + color: area.color || '#3388ff', + weight: area.weight || 3, + opacity: typeof area.opacity !== 'undefined' ? area.opacity : 1, + fill: typeof area.fill !== 'undefined' ? !!area.fill : true, + fillColor: area.fillColor || area.color || '#3388ff', + fillOpacity: area.fillOpacity || 0.2, + fillRule: area.fillRule, + }, + points: area.points, + outline: false, + + tooltipContent: area.tooltip, + popupContent: area.popup, + isPopupHTML: true, + }); + } + + private static buildLine(line: any): LiveAtlasLine { + return Object.seal({ + style: { + stroke: typeof line.stroke !== 'undefined' ? !!line.stroke : true, + color: line.color || '#3388ff', + weight: line.weight || 3, + opacity: typeof line.opacity !== 'undefined' ? line.opacity : 1, + }, + points: line.points, + + tooltipContent: line.tooltip, + popupContent: line.popup, + isPopupHTML: true, + }); + } + + private static buildCircle(circle: any): LiveAtlasCircle { + return Object.seal({ + location: { + x: circle.center?.x || 0, + y: 0, + z: circle.center?.z || 0, + }, + radius: [circle.radiusX || circle.radius || 0, circle.radiusZ || circle.radius || 0], + style: { + stroke: typeof circle.stroke !== 'undefined' ? !!circle.stroke : true, + color: circle.color || '#3388ff', + weight: circle.weight || 3, + opacity: typeof circle.opacity !== 'undefined' ? circle.opacity : 1, + fill: typeof circle.stroke !== 'undefined' ? !!circle.stroke : true, + fillColor: circle.fillColor || circle.color || '#3388ff', + fillOpacity: circle.fillOpacity || 0.2, + fillRule: circle.fillRule, + }, + + tooltipContent: circle.tooltip, + popupContent: circle.popup, + isPopupHTML: true + }); + } + + async loadServerConfiguration(): Promise { + if(this.configurationAbort) { + this.configurationAbort.abort(); + } + + this.configurationAbort = new AbortController(); + + const baseUrl = this.config.pl3xmap, + response = await Pl3xmapMapProvider.fetchJSON(`${baseUrl}tiles/settings.json`, this.configurationAbort.signal); + + if (response.error) { + throw new Error(response.error); + } + + const config = Pl3xmapMapProvider.buildServerConfig(response), + worldNames: string[] = (response.worlds || []).filter((world: any) => world && !!world.name) + .map((world: any) => world.name); + + const worldResponses = await Promise.all(worldNames.map(name => + Pl3xmapMapProvider.fetchJSON(`${baseUrl}tiles/${name}/settings.json`, this.configurationAbort!.signal))); + + this.store.commit(MutationTypes.SET_SERVER_CONFIGURATION, config); + this.store.commit(MutationTypes.SET_SERVER_MESSAGES, Pl3xmapMapProvider.buildMessagesConfig(response)); + this.store.commit(MutationTypes.SET_WORLDS, this.buildWorlds(response, worldResponses)); + this.store.commit(MutationTypes.SET_COMPONENTS, Pl3xmapMapProvider.buildComponents(response)); + + //Pl3xmap has no login functionality + this.store.commit(MutationTypes.SET_LOGGED_IN, false); + } + + async populateWorld(world: LiveAtlasWorldDefinition) { + const markerSets = await this.getMarkerSets(world), + worldConfig = this.worldSettings.get(world.name); + + this.store.commit(MutationTypes.SET_MARKER_SETS, markerSets); + this.store.commit(MutationTypes.SET_COMPONENTS, worldConfig!.components); + } + + private async getPlayers(): Promise> { + const url = `${this.config.pl3xmap}/tiles/players.json`; + + if(this.playersAbort) { + this.playersAbort.abort(); + } + + this.playersAbort = new AbortController(); + + const response = await Pl3xmapMapProvider.fetchJSON(url, this.playersAbort.signal), + players: Set = new Set(); + + (response.players || []).forEach((player: any) => { + console.log(player.uuid); + players.add({ + name: (player.name || '').toLowerCase(), + uuid: player.uuid, + displayName: player.name || "", + health: player.health || 0, + armor: player.armor || 0, + sort: 0, + hidden: false, + location: { + //Add 0.5 to position in the middle of a block + x: !isNaN(player.x) ? player.x + 0.5 : 0, + y: 0, + z: !isNaN(player.z) ? player.z + 0.5 : 0, + world: player.world, + } + }); + }); + + // Extra fake players for testing + // for(let i = 0; i < 450; i++) { + // players.add({ + // name: "VIDEO GAMES " + i, + // displayName: "VIDEO GAMES " + i, + // health: Math.round(Math.random() * 10), + // armor: Math.round(Math.random() * 10), + // sort: Math.round(Math.random() * 10), + // hidden: false, + // location: { + // x: Math.round(Math.random() * 1000) - 500, + // y: 0, + // z: Math.round(Math.random() * 1000) - 500, + // world: "world", + // } + // }); + // } + + this.store.commit(MutationTypes.SET_MAX_PLAYERS, response.max || 0); + + return players; + } + + sendChatMessage(message: string) { + throw new Error('Pl3xmap does not support chat'); + } + + startUpdates() { + this.updatesEnabled = true; + this.update(); + } + + private async update() { + try { + const players = await this.getPlayers(); + + this.updateTimestamp = new Date(); + + await this.store.dispatch(ActionTypes.SET_PLAYERS, players); + } finally { + if(this.updatesEnabled) { + if(this.updateTimeout) { + clearTimeout(this.updateTimeout); + } + + this.updateTimeout = setTimeout(() => this.update(), this.updateInterval); + } + } + } + + stopUpdates() { + this.updatesEnabled = false; + + if (this.updateTimeout) { + clearTimeout(this.updateTimeout); + } + + this.updateTimeout = 0; + } + + getTilesUrl(): string { + return `${this.config.pl3xmap}tiles/`; + } + + getPlayerHeadUrl(head: HeadQueueEntry): string { + //TODO: Listen to config + return 'https://mc-heads.net/avatar/{uuid}/16'.replace('{uuid}', head.uuid || ''); + } + + getMarkerIconUrl(icon: string): string { + return `${this.config.pl3xmap}images/icon/registered/${icon}.png`; + } + + destroy() { + super.destroy(); + + if(this.configurationAbort) { + this.configurationAbort.abort(); + } + + if(this.playersAbort) { + this.playersAbort.abort(); + } + + if(this.markersAbort) { + this.markersAbort.abort(); + } + } +} diff --git a/src/store/mutations.ts b/src/store/mutations.ts index 1e1fca2..d9972f0 100644 --- a/src/store/mutations.ts +++ b/src/store/mutations.ts @@ -42,6 +42,7 @@ import { LiveAtlasServerConfig, LiveAtlasChat, LiveAtlasPartialComponentConfig, LiveAtlasComponentConfig } from "@/index"; import DynmapMapProvider from "@/providers/DynmapMapProvider"; +import Pl3xmapMapProvider from "@/providers/Pl3xmapMapProvider"; export type CurrentMapPayload = { worldName: string; @@ -497,8 +498,16 @@ export const mutations: MutationTree & Mutations = { state.currentMapProvider.destroy(); } - state.currentMapProvider = Object.seal( - new DynmapMapProvider(state.servers.get(serverName) as LiveAtlasDynmapServerDefinition)); + switch(state.currentServer!.type) { + case 'pl3xmap': + state.currentMapProvider = Object.seal( + new Pl3xmapMapProvider(state.servers.get(serverName) as LiveAtlasServerDefinition)); + break; + case 'dynmap': + state.currentMapProvider = Object.seal( + new DynmapMapProvider(state.servers.get(serverName) as LiveAtlasServerDefinition)); + break; + } }, //Sets the currently active map/world diff --git a/src/util/config.ts b/src/util/config.ts index 8610381..d7964ff 100644 --- a/src/util/config.ts +++ b/src/util/config.ts @@ -38,40 +38,37 @@ const validateLiveAtlasConfiguration = (config: any): Map