diff --git a/.idea/copyright/Dynmap.xml b/.idea/copyright/Dynmap.xml
new file mode 100644
index 0000000..6c668e8
--- /dev/null
+++ b/.idea/copyright/Dynmap.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/copyright/Original.xml b/.idea/copyright/Original.xml
new file mode 100644
index 0000000..c7bcb6a
--- /dev/null
+++ b/.idea/copyright/Original.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/copyright/profiles_settings.xml b/.idea/copyright/profiles_settings.xml
new file mode 100644
index 0000000..58a1b80
--- /dev/null
+++ b/.idea/copyright/profiles_settings.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/scopes/Dynmap.xml b/.idea/scopes/Dynmap.xml
new file mode 100644
index 0000000..3cb6054
--- /dev/null
+++ b/.idea/scopes/Dynmap.xml
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/.idea/scopes/Original.xml b/.idea/scopes/Original.xml
new file mode 100644
index 0000000..809d5ac
--- /dev/null
+++ b/.idea/scopes/Original.xml
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/src/App.vue b/src/App.vue
index 8dc826d..f22bbee 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -1,17 +1,17 @@
@@ -43,21 +43,19 @@ export default defineComponent({
setup() {
const store = useStore(),
- updateInterval = computed(() => store.state.configuration.updateInterval),
title = computed(() => store.state.configuration.title),
currentUrl = computed(() => store.getters.url),
currentServer = computed(() => store.state.currentServer),
configurationHash = computed(() => store.state.configurationHash),
chatBoxEnabled = computed(() => store.state.components.chatBox),
chatVisible = computed(() => store.state.ui.visibleElements.has('chat')),
- updatesEnabled = ref(false),
- updateTimeout = ref(0),
configAttempts = ref(0),
loadConfiguration = async () => {
try {
await store.dispatch(ActionTypes.LOAD_CONFIGURATION, undefined);
- startUpdates();
+ await store.dispatch(ActionTypes.START_UPDATES, undefined);
+
requestAnimationFrame(() => {
hideSplash();
@@ -80,36 +78,6 @@ export default defineComponent({
}
},
- startUpdates = () => {
- updatesEnabled.value = true;
- update();
- },
-
- update = async () => {
- //TODO: Error notification for repeated failures?
- try {
- await store.dispatch(ActionTypes.GET_UPDATE, undefined);
- } finally {
- if(updatesEnabled.value) {
- if(updateTimeout.value) {
- clearTimeout(updateTimeout.value);
- }
-
- updateTimeout.value = setTimeout(() => update(), updateInterval.value);
- }
- }
- },
-
- stopUpdates = () => {
- updatesEnabled.value = false;
-
- if (updateTimeout.value) {
- clearTimeout(updateTimeout.value);
- }
-
- updateTimeout.value = 0;
- },
-
handleUrl = () => {
const parsedUrl = parseUrl();
@@ -174,7 +142,6 @@ export default defineComponent({
watch(currentUrl, (url) => window.history.replaceState({}, '', url));
watch(currentServer, (newServer?: LiveAtlasServerDefinition) => {
showSplash();
- stopUpdates();
if(!newServer) {
return;
@@ -182,6 +149,7 @@ export default defineComponent({
//Cleanup
store.commit(MutationTypes.CLEAR_PLAYERS, undefined);
+ store.commit(MutationTypes.SET_MAX_PLAYERS, 0);
store.commit(MutationTypes.CLEAR_CURRENT_MAP, undefined);
store.commit(MutationTypes.CLEAR_PARSED_URL, undefined);
store.commit(MutationTypes.CLEAR_WORLDS, undefined);
@@ -190,17 +158,17 @@ export default defineComponent({
window.history.replaceState({}, '', newServer.id);
loadConfiguration();
}, {deep: true});
- watch(configurationHash, (newHash, oldHash) => {
+ watch(configurationHash, async (newHash, oldHash) => {
if(newHash && oldHash) {
showSplash();
- stopUpdates();
store.commit(MutationTypes.CLEAR_PARSED_URL, undefined);
- loadConfiguration();
+ await store.dispatch(ActionTypes.STOP_UPDATES, undefined);
+ await loadConfiguration();
}
});
onMounted(() => loadConfiguration());
- onBeforeUnmount(() => stopUpdates());
+ onBeforeUnmount(() => store.dispatch(ActionTypes.STOP_UPDATES, undefined));
handleUrl();
onResize();
diff --git a/src/api.ts b/src/api.ts
deleted file mode 100644
index cc7acc7..0000000
--- a/src/api.ts
+++ /dev/null
@@ -1,777 +0,0 @@
-/*
- * Copyright 2020 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 {
- DynmapArea,
- DynmapChat,
- DynmapCircle,
- DynmapComponentConfig,
- DynmapConfigurationResponse,
- DynmapLine,
- DynmapMarker,
- DynmapMarkerSet,
- DynmapMarkerSetUpdates,
- DynmapPlayer,
- DynmapServerConfig,
- DynmapTileUpdate,
- DynmapUpdate,
- DynmapUpdateResponse,
- DynmapUpdates
-} from "@/dynmap";
-import {useStore} from "@/store";
-import ChatError from "@/errors/ChatError";
-import {LiveAtlasDimension, LiveAtlasServerMessageConfig, LiveAtlasWorldDefinition} from "@/index";
-import LiveAtlasMapDefinition from "@/model/LiveAtlasMapDefinition";
-
-const titleColours = /§[0-9a-f]/ig,
- netherWorldName = /_?nether(_|$)/i,
- endWorldName = /(^|_)end(_|$)/i;
-
-function buildServerConfig(response: any): DynmapServerConfig {
- return {
- version: response.dynmapversion || '',
- grayHiddenPlayers: response.grayplayerswhenhidden || false,
- defaultMap: response.defaultmap || undefined,
- defaultWorld: response.defaultworld || undefined,
- defaultZoom: response.defaultzoom || 0,
- followMap: response.followmap || undefined,
- followZoom: response.followzoom || 0,
- updateInterval: response.updaterate || 3000,
- showLayerControl: response.showlayercontrol && response.showlayercontrol !== 'false', //Sent as a string for some reason
- title: response.title.replace(titleColours, '') || 'Dynmap',
- loginEnabled: response['login-enabled'] || false,
- maxPlayers: response.maxcount || 0,
- expandUI: response.sidebaropened && response.sidebaropened !== 'false', //Sent as a string for some reason
- hash: response.confighash || 0,
- };
-}
-
-function buildMessagesConfig(response: any): LiveAtlasServerMessageConfig {
- return {
- chatPlayerJoin: response.joinmessage || '',
- chatPlayerQuit: response.quitmessage || '',
- chatAnonymousJoin: response['msg-hiddennamejoin'] || '',
- chatAnonymousQuit: response['msg-hiddennamequit'] || '',
- chatErrorNotAllowed: response['msg-chatnotallowed'] || '',
- chatErrorRequiresLogin: response['msg-chatrequireslogin'] || '',
- chatErrorCooldown: response.spammessage || '',
- worldsHeading: response['msg-maptypes'] || '',
- playersHeading: response['msg-players'] || '',
- }
-}
-
-function buildWorlds(response: any): Array {
- const worlds: Map = new Map();
-
- //Get all the worlds first so we can handle append_to_world properly
- (response.worlds || []).forEach((world: any) => {
- let worldType: LiveAtlasDimension = 'overworld';
-
- if (netherWorldName.test(world.name) || (world.name == 'DIM-1')) {
- worldType = 'nether';
- } else if (endWorldName.test(world.name) || (world.name == 'DIM1')) {
- worldType = 'end';
- }
-
- worlds.set(world.name, {
- seaLevel: world.sealevel || 64,
- name: world.name,
- dimension: worldType,
- protected: world.protected || false,
- title: world.title || '',
- height: world.height || 256,
- center: {
- x: world.center.x || 0,
- y: world.center.y || 0,
- z: world.center.z || 0
- },
- maps: new Map(),
- });
- });
-
- (response.worlds || []).forEach((world: any) => {
- (world.maps || []).forEach((map: any) => {
- const worldName = map.append_to_world || world.name,
- w = worlds.get(worldName);
-
- if(!w) {
- console.warn(`Ignoring map '${map.name}' associated with non-existent world '${worldName}'`);
- return;
- }
-
- w.maps.set(map.name, new LiveAtlasMapDefinition({
- world: world, //Ignore append_to_world here otherwise things break
- background: map.background || '#000000',
- backgroundDay: map.backgroundday || '#000000',
- backgroundNight: map.backgroundnight || '#000000',
- icon: map.icon || undefined,
- imageFormat: map['image-format'] || 'png',
- name: map.name || '(Unnamed map)',
- nightAndDay: map.nightandday || false,
- prefix: map.prefix || '',
- protected: map.protected || false,
- title: map.title || '',
- mapToWorld: map.maptoworld || undefined,
- worldToMap: map.worldtomap || undefined,
- nativeZoomLevels: map.mapzoomout || 1,
- extraZoomLevels: map.mapzoomin || 0
- }));
- });
- });
-
- return Array.from(worlds.values());
-}
-
-function buildComponents(response: any): DynmapComponentConfig {
- const components: DynmapComponentConfig = {
- markers: {
- showLabels: false,
- },
- chatBox: undefined,
- chatBalloons: false,
- playerMarkers: undefined,
- coordinatesControl: undefined,
- linkControl: false,
- clockControl: undefined,
- logoControls: [],
- };
-
- (response.components || []).forEach((component: any) => {
- const type = component.type || "unknown";
-
- switch (type) {
- case "markers":
- components.markers = {
- showLabels: component.showlabel || false,
- }
-
- break;
-
- case "playermarkers":
- components.playerMarkers = {
- hideByDefault: component.hidebydefault || false,
- layerName: component.label || "Players",
- layerPriority: component.layerprio || 0,
- showBodies: component.showplayerbody || false,
- showSkinFaces: component.showplayerfaces || false,
- showHealth: component.showplayerhealth || false,
- smallFaces: component.smallplayerfaces || false,
- }
-
- break;
-
- case "coord":
- components.coordinatesControl = {
- showY: !(component.hidey || false),
- label: component.label || "Location: ",
- showRegion: component['show-mcr'] || false,
- showChunk: component['show-chunk'] || false,
- }
-
- break;
-
- case "link":
- components.linkControl = true;
-
- break;
-
- case "digitalclock":
- components.clockControl = {
- showDigitalClock: true,
- showWeather: false,
- showTimeOfDay: false,
- }
- break;
-
- case "timeofdayclock":
- components.clockControl = {
- showTimeOfDay: true,
- showDigitalClock: component.showdigitalclock || false,
- showWeather: component.showweather || false,
- }
- break;
-
- case "logo":
- components.logoControls.push({
- text: component.text || '',
- url: component.linkurl || undefined,
- position: component.position.replace('-', '') || 'topleft',
- image: component.logourl || undefined,
- });
- break;
-
- case "chat":
- if (response.allowwebchat) {
- components.chatSending = {
- loginRequired: response['webchat-requires-login'] || false,
- maxLength: response['chatlengthlimit'] || 256,
- cooldown: response['webchat-interval'] || 5,
- }
- }
- break;
-
- case "chatbox":
- components.chatBox = {
- allowUrlName: component.allowurlname || false,
- showPlayerFaces: component.showplayerfaces || false,
- messageLifetime: component.messagettl || Infinity,
- messageHistory: component.scrollback || Infinity,
- }
- break;
-
- case "chatballoon":
- components.chatBalloons = true;
- }
- });
-
- return components;
-}
-
-function buildMarkerSet(id: string, data: any): any {
- return {
- id,
- label: data.label || "Unnamed set",
- hidden: data.hide || false,
- priority: data.layerprio || 0,
- showLabels: data.showlabels || undefined,
- minZoom: typeof data.minzoom !== 'undefined' && data.minzoom > -1 ? data.minzoom : undefined,
- maxZoom: typeof data.maxzoom !== 'undefined' && data.maxzoom > -1 ? data.maxzoom : undefined,
- }
-}
-
-function buildMarkers(data: any): Map {
- const markers = Object.freeze(new Map()) as Map;
-
- for (const key in data) {
- if (!Object.prototype.hasOwnProperty.call(data, key)) {
- continue;
- }
-
- markers.set(key, buildMarker(data[key]));
- }
-
- return markers;
-}
-
-function buildMarker(marker: any): DynmapMarker {
- return {
- label: marker.label || '',
- location: {
- x: marker.x || 0,
- y: marker.y || 0,
- z: marker.z || 0,
- },
- dimensions: marker.dim ? marker.dim.split('x') : [16, 16],
- icon: marker.icon || "default",
- isHTML: marker.markup || false,
- minZoom: typeof marker.minzoom !== 'undefined' && marker.minzoom > -1 ? marker.minzoom : undefined,
- maxZoom: typeof marker.maxzoom !== 'undefined' && marker.maxzoom > -1 ? marker.maxzoom : undefined,
- popupContent: marker.desc || undefined,
- };
-}
-
-function buildAreas(data: any): Map {
- const areas = Object.freeze(new Map()) as Map;
-
- for (const key in data) {
- if (!Object.prototype.hasOwnProperty.call(data, key)) {
- continue;
- }
-
- areas.set(key, buildArea(data[key]));
- }
-
- return areas;
-}
-
-function buildArea(area: any): DynmapArea {
- return {
- style: {
- color: area.color || '#ff0000',
- opacity: area.opacity || 1,
- weight: area.weight || 1,
- fillColor: area.fillcolor || '#ff0000',
- fillOpacity: area.fillopacity || 0,
- },
- label: area.label || '',
- isHTML: area.markup || false,
- x: area.x || [0, 0],
- y: [area.ybottom || 0, area.ytop || 0],
- z: area.z || [0, 0],
- minZoom: typeof area.minzoom !== 'undefined' && area.minzoom > -1 ? area.minzoom : undefined,
- maxZoom: typeof area.maxzoom !== 'undefined' && area.maxzoom > -1 ? area.maxzoom : undefined,
- popupContent: area.desc || undefined,
- };
-}
-
-function buildLines(data: any): Map {
- const lines = Object.freeze(new Map()) as Map;
-
- for (const key in data) {
- if (!Object.prototype.hasOwnProperty.call(data, key)) {
- continue;
- }
-
- lines.set(key, buildLine(data[key]));
- }
-
- return lines;
-}
-
-function buildLine(line: any): DynmapLine {
- return {
- x: line.x || [0, 0],
- y: line.y || [0, 0],
- z: line.z || [0, 0],
- style: {
- color: line.color || '#ff0000',
- opacity: line.opacity || 1,
- weight: line.weight || 1,
- },
- label: line.label || '',
- isHTML: line.markup || false,
- minZoom: typeof line.minzoom !== 'undefined' && line.minzoom > -1 ? line.minzoom : undefined,
- maxZoom: typeof line.maxzoom !== 'undefined' && line.maxzoom > -1 ? line.maxzoom : undefined,
- popupContent: line.desc || undefined,
- };
-}
-
-function buildCircles(data: any): Map {
- const circles = Object.freeze(new Map()) as Map;
-
- for (const key in data) {
- if (!Object.prototype.hasOwnProperty.call(data, key)) {
- continue;
- }
-
- circles.set(key, buildCircle(data[key]));
- }
-
- return circles;
-}
-
-function buildCircle(circle: any): DynmapCircle {
- return {
- location: {
- x: circle.x || 0,
- y: circle.y || 0,
- z: circle.z || 0,
- },
- radius: [circle.xr || 0, circle.zr || 0],
- style: {
- fillColor: circle.fillcolor || '#ff0000',
- fillOpacity: circle.fillopacity || 0,
- color: circle.color || '#ff0000',
- opacity: circle.opacity || 1,
- weight: circle.weight || 1,
- },
- label: circle.label || '',
- isHTML: circle.markup || false,
-
- minZoom: typeof circle.minzoom !== 'undefined' && circle.minzoom > -1 ? circle.minzoom : undefined,
- maxZoom: typeof circle.maxzoom !== 'undefined' && circle.maxzoom > -1 ? circle.maxzoom : undefined,
- popupContent: circle.desc || undefined,
- };
-}
-
-function buildUpdates(data: Array): DynmapUpdates {
- const updates = {
- markerSets: new Map(),
- tiles: [] as DynmapTileUpdate[],
- chat: [] as DynmapChat[],
- },
- dropped = {
- stale: 0,
- noSet: 0,
- noId: 0,
- unknownType: 0,
- unknownCType: 0,
- incomplete: 0,
- notImplemented: 0,
- },
- lastUpdate = useStore().state.updateTimestamp;
-
- let accepted = 0;
-
- for (const entry of data) {
- switch (entry.type) {
- case 'component': {
- if (lastUpdate && entry.timestamp < lastUpdate) {
- dropped.stale++;
- continue;
- }
-
- if (!entry.id) {
- dropped.noId++;
- continue;
- }
-
- //Set updates don't have a set field, the id is the set
- const set = entry.msg.startsWith("set") ? entry.id : entry.set;
-
- if (!set) {
- dropped.noSet++;
- continue;
- }
-
- if (entry.ctype !== 'markers') {
- dropped.unknownCType++;
- continue;
- }
-
- if (!updates.markerSets.has(set)) {
- updates.markerSets.set(set, {
- areaUpdates: [],
- markerUpdates: [],
- lineUpdates: [],
- circleUpdates: [],
- removed: false,
- });
- }
-
- const markerSetUpdates = updates.markerSets.get(set),
- update: DynmapUpdate = {
- id: entry.id,
- removed: entry.msg.endsWith('deleted'),
- };
-
- if (entry.msg.startsWith("set")) {
- markerSetUpdates!.removed = update.removed;
- markerSetUpdates!.payload = update.removed ? undefined : buildMarkerSet(set, entry);
- } else if (entry.msg.startsWith("marker")) {
- update.payload = update.removed ? undefined : buildMarker(entry);
- markerSetUpdates!.markerUpdates.push(Object.freeze(update));
- } else if (entry.msg.startsWith("area")) {
- update.payload = update.removed ? undefined : buildArea(entry);
- markerSetUpdates!.areaUpdates.push(Object.freeze(update));
-
- } else if (entry.msg.startsWith("circle")) {
- update.payload = update.removed ? undefined : buildCircle(entry);
- markerSetUpdates!.circleUpdates.push(Object.freeze(update));
-
- } else if (entry.msg.startsWith("line")) {
- update.payload = update.removed ? undefined : buildLine(entry);
- markerSetUpdates!.lineUpdates.push(Object.freeze(update));
- }
-
- accepted++;
-
- break;
- }
-
- case 'chat':
- if (!entry.message || !entry.timestamp) {
- dropped.incomplete++;
- continue;
- }
-
- if (entry.timestamp < lastUpdate) {
- dropped.stale++;
- continue;
- }
-
- if (entry.source !== 'player' && entry.source !== 'web') {
- dropped.notImplemented++;
- continue;
- }
-
- updates.chat.push({
- type: 'chat',
- source: entry.source || undefined,
- playerAccount: entry.account || undefined,
- playerName: entry.playerName || undefined,
- message: entry.message || "",
- timestamp: entry.timestamp,
- channel: entry.channel || undefined,
- });
- break;
-
- case 'playerjoin':
- if (!entry.account || !entry.timestamp) {
- dropped.incomplete++;
- continue;
- }
-
- if (entry.timestamp < lastUpdate) {
- dropped.stale++;
- continue;
- }
-
- updates.chat.push({
- type: 'playerjoin',
- playerAccount: entry.account,
- playerName: entry.playerName || "",
- timestamp: entry.timestamp || undefined,
- });
- break;
-
- case 'playerquit':
- if (!entry.account || !entry.timestamp) {
- dropped.incomplete++;
- continue;
- }
-
- if (entry.timestamp < lastUpdate) {
- dropped.stale++;
- continue;
- }
-
- updates.chat.push({
- type: 'playerleave',
- playerAccount: entry.account,
- playerName: entry.playerName || "",
- timestamp: entry.timestamp || undefined,
- });
- break;
-
- case 'tile':
- if (!entry.name || !entry.timestamp) {
- dropped.incomplete++;
- continue;
- }
-
- if (lastUpdate && entry.timestamp < lastUpdate) {
- dropped.stale++;
- continue;
- }
-
- updates.tiles.push({
- name: entry.name,
- timestamp: entry.timestamp,
- });
-
- accepted++;
- break;
-
- default:
- dropped.unknownType++;
- }
- }
-
- //Sort chat by newest first
- updates.chat = updates.chat.sort((one, two) => {
- return two.timestamp - one.timestamp;
- });
-
- console.debug(`Updates: ${accepted} accepted. Rejected: `, dropped);
-
- return updates;
-}
-
-async function fetchJSON(url: string, signal: AbortSignal) {
- let response, json;
-
- try {
- response = await fetch(url, {signal});
- } catch(e) {
- if(e instanceof DOMException && e.name === 'AbortError') {
- console.warn(`Request aborted (${url}`);
- throw e;
- } else {
- console.error(e);
- }
-
- throw new Error(`Network request failed`);
- }
-
- if (!response.ok) {
- throw new Error(`Network request failed (${response.statusText || 'Unknown'})`);
- }
-
- try {
- json = await response.json();
- } catch(e) {
- if(e instanceof DOMException && e.name === 'AbortError') {
- console.warn(`Request aborted (${url}`);
- throw e;
- } else {
- throw new Error('Request returned invalid json');
- }
- }
-
- return json;
-}
-
-let configurationAbort: AbortController | undefined = undefined,
- markersAbort: AbortController | undefined = undefined,
- updateAbort: AbortController | undefined = undefined;
-
-export default {
- async getConfiguration(): Promise {
- if(configurationAbort) {
- configurationAbort.abort();
- }
-
- configurationAbort = new AbortController();
-
- const response = await fetchJSON(useStore().getters.serverConfig.dynmap.configuration, configurationAbort.signal);
-
- if (response.error === 'login-required') {
- throw new Error("Login required");
- } else if (response.error) {
- throw new Error(response.error);
- }
-
- return {
- config: buildServerConfig(response),
- messages: buildMessagesConfig(response),
- worlds: buildWorlds(response),
- components: buildComponents(response),
- loggedIn: response.loggedin || false,
- }
- },
-
- async getUpdate(requestId: number, world: string, timestamp: number): Promise {
- let url = useStore().getters.serverConfig.dynmap.update;
- url = url.replace('{world}', world);
- url = url.replace('{timestamp}', timestamp.toString());
-
- if(updateAbort) {
- updateAbort.abort();
- }
-
- updateAbort = new AbortController();
-
- const response = await fetchJSON(url, updateAbort.signal);
- const players: Set = new Set();
-
- (response.players || []).forEach((player: any) => {
- const world = player.world && player.world !== '-some-other-bogus-world-' ? player.world : undefined;
-
- players.add({
- account: player.account || "",
- health: player.health || 0,
- armor: player.armor || 0,
- name: player.name || "",
- sort: player.sort || 0,
- hidden: !world,
- location: {
- //Add 0.5 to position in the middle of a block
- x: !isNaN(player.x) ? player.x + 0.5 : 0,
- y: !isNaN(player.y) ? player.y : 0,
- z: !isNaN(player.z) ? player.z + 0.5 : 0,
- world: world,
- }
- });
- });
-
- //Extra fake players for testing
- // for(let i = 0; i < 450; i++) {
- // players.add({
- // account: "VIDEO GAMES " + i,
- // health: Math.round(Math.random() * 10),
- // armor: Math.round(Math.random() * 10),
- // name: "VIDEO GAMES " + i,
- // sort: Math.round(Math.random() * 10),
- // hidden: false,
- // location: {
- // x: Math.round(Math.random() * 1000) - 500,
- // y: 64,
- // z: Math.round(Math.random() * 1000) - 500,
- // world: "world",
- // }
- // });
- // }
-
- return {
- worldState: {
- timeOfDay: response.servertime || 0,
- thundering: response.isThundering || false,
- raining: response.hasStorm || false,
- },
- playerCount: response.count || 0,
- configHash: response.confighash || 0,
- timestamp: response.timestamp || 0,
- players,
- updates: buildUpdates(response.updates || []),
- }
- },
-
- async getMarkerSets(world: string): Promise