001/*
002 * PlotSquared, a land and world management plugin for Minecraft.
003 * Copyright (C) IntellectualSites <https://intellectualsites.com>
004 * Copyright (C) IntellectualSites team and contributors
005 *
006 * This program is free software: you can redistribute it and/or modify
007 * it under the terms of the GNU General Public License as published by
008 * the Free Software Foundation, either version 3 of the License, or
009 * (at your option) any later version.
010 *
011 * This program is distributed in the hope that it will be useful,
012 * but WITHOUT ANY WARRANTY; without even the implied warranty of
013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
014 * GNU General Public License for more details.
015 *
016 * You should have received a copy of the GNU General Public License
017 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
018 */
019package com.plotsquared.core;
020
021import com.plotsquared.core.configuration.ConfigurationSection;
022import com.plotsquared.core.configuration.ConfigurationUtil;
023import com.plotsquared.core.configuration.MemorySection;
024import com.plotsquared.core.configuration.Settings;
025import com.plotsquared.core.configuration.Storage;
026import com.plotsquared.core.configuration.caption.CaptionMap;
027import com.plotsquared.core.configuration.caption.DummyCaptionMap;
028import com.plotsquared.core.configuration.caption.TranslatableCaption;
029import com.plotsquared.core.configuration.caption.load.CaptionLoader;
030import com.plotsquared.core.configuration.caption.load.DefaultCaptionProvider;
031import com.plotsquared.core.configuration.file.YamlConfiguration;
032import com.plotsquared.core.configuration.serialization.ConfigurationSerialization;
033import com.plotsquared.core.database.DBFunc;
034import com.plotsquared.core.database.Database;
035import com.plotsquared.core.database.MySQL;
036import com.plotsquared.core.database.SQLManager;
037import com.plotsquared.core.database.SQLite;
038import com.plotsquared.core.generator.GeneratorWrapper;
039import com.plotsquared.core.generator.HybridPlotWorld;
040import com.plotsquared.core.generator.HybridUtils;
041import com.plotsquared.core.generator.IndependentPlotGenerator;
042import com.plotsquared.core.inject.factory.HybridPlotWorldFactory;
043import com.plotsquared.core.listener.PlotListener;
044import com.plotsquared.core.location.Location;
045import com.plotsquared.core.player.PlayerMetaDataKeys;
046import com.plotsquared.core.plot.BlockBucket;
047import com.plotsquared.core.plot.Plot;
048import com.plotsquared.core.plot.PlotArea;
049import com.plotsquared.core.plot.PlotAreaTerrainType;
050import com.plotsquared.core.plot.PlotAreaType;
051import com.plotsquared.core.plot.PlotCluster;
052import com.plotsquared.core.plot.PlotId;
053import com.plotsquared.core.plot.PlotManager;
054import com.plotsquared.core.plot.expiration.ExpireManager;
055import com.plotsquared.core.plot.expiration.ExpiryTask;
056import com.plotsquared.core.plot.flag.GlobalFlagContainer;
057import com.plotsquared.core.plot.world.PlotAreaManager;
058import com.plotsquared.core.plot.world.SinglePlotArea;
059import com.plotsquared.core.plot.world.SinglePlotAreaManager;
060import com.plotsquared.core.util.EventDispatcher;
061import com.plotsquared.core.util.FileUtils;
062import com.plotsquared.core.util.LegacyConverter;
063import com.plotsquared.core.util.MathMan;
064import com.plotsquared.core.util.ReflectionUtils;
065import com.plotsquared.core.util.task.TaskManager;
066import com.plotsquared.core.uuid.UUIDPipeline;
067import com.sk89q.worldedit.WorldEdit;
068import com.sk89q.worldedit.event.platform.PlatformReadyEvent;
069import com.sk89q.worldedit.math.BlockVector2;
070import com.sk89q.worldedit.util.eventbus.EventHandler;
071import com.sk89q.worldedit.util.eventbus.Subscribe;
072import org.apache.logging.log4j.LogManager;
073import org.apache.logging.log4j.Logger;
074import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
075import org.checkerframework.checker.nullness.qual.NonNull;
076import org.checkerframework.checker.nullness.qual.Nullable;
077
078import java.io.BufferedReader;
079import java.io.File;
080import java.io.FileInputStream;
081import java.io.FileOutputStream;
082import java.io.IOException;
083import java.io.InputStream;
084import java.io.InputStreamReader;
085import java.io.ObjectInputStream;
086import java.io.ObjectOutputStream;
087import java.net.URI;
088import java.net.URISyntaxException;
089import java.net.URL;
090import java.nio.file.Files;
091import java.nio.file.StandardOpenOption;
092import java.sql.SQLException;
093import java.util.ArrayDeque;
094import java.util.ArrayList;
095import java.util.Arrays;
096import java.util.Collection;
097import java.util.Collections;
098import java.util.Comparator;
099import java.util.HashMap;
100import java.util.HashSet;
101import java.util.Iterator;
102import java.util.List;
103import java.util.Locale;
104import java.util.Map;
105import java.util.Map.Entry;
106import java.util.Objects;
107import java.util.Set;
108import java.util.concurrent.Executors;
109import java.util.function.Consumer;
110import java.util.regex.Pattern;
111import java.util.zip.ZipEntry;
112import java.util.zip.ZipInputStream;
113
114/**
115 * An implementation of the core, with a static getter for easy access.
116 */
117@SuppressWarnings({"WeakerAccess"})
118public class PlotSquared {
119
120    private static final Logger LOGGER = LogManager.getLogger("PlotSquared/" + PlotSquared.class.getSimpleName());
121    private static @MonotonicNonNull PlotSquared instance;
122
123    // Implementation
124    private final PlotPlatform<?> platform;
125    // Current thread
126    private final Thread thread;
127    // UUID pipelines
128    private final UUIDPipeline impromptuUUIDPipeline =
129            new UUIDPipeline(Executors.newCachedThreadPool());
130    private final UUIDPipeline backgroundUUIDPipeline =
131            new UUIDPipeline(Executors.newSingleThreadExecutor());
132    // Localization
133    private final Map<String, CaptionMap> captionMaps = new HashMap<>();
134    public HashMap<String, HashMap<PlotId, Plot>> plots_tmp;
135    private CaptionLoader captionLoader;
136    // WorldEdit instance
137    private WorldEdit worldedit;
138    private File configFile;
139    private File worldsFile;
140    private YamlConfiguration worldConfiguration;
141    // Temporary hold the plots/clusters before the worlds load
142    private HashMap<String, Set<PlotCluster>> clustersTmp;
143    private YamlConfiguration config;
144    // Platform / Version / Update URL
145    private PlotVersion version;
146    // Files and configuration
147    private File jarFile = null; // This file
148    private File storageFile;
149    private EventDispatcher eventDispatcher;
150    private PlotListener plotListener;
151
152    private boolean weInitialised;
153
154    /**
155     * Initialize PlotSquared with the desired Implementation class.
156     *
157     * @param iPlotMain Implementation of {@link PlotPlatform} used
158     * @param platform  The platform being used
159     */
160    public PlotSquared(
161            final @NonNull PlotPlatform<?> iPlotMain,
162            final @NonNull String platform
163    ) {
164        if (instance != null) {
165            throw new IllegalStateException("Cannot re-initialize the PlotSquared singleton");
166        }
167        instance = this;
168
169        this.thread = Thread.currentThread();
170        this.platform = iPlotMain;
171        Settings.PLATFORM = platform;
172
173        // Initialize the class
174        PlayerMetaDataKeys.load();
175
176        //
177        // Register configuration serializable classes
178        //
179        ConfigurationSerialization.registerClass(BlockBucket.class, "BlockBucket");
180
181        // load configs before reading from settings
182        if (!setupConfigs()) {
183            return;
184        }
185
186        this.captionLoader = CaptionLoader.of(
187                Locale.ENGLISH,
188                CaptionLoader.patternExtractor(Pattern.compile("messages_(.*)\\.json")),
189                DefaultCaptionProvider.forClassLoaderFormatString(
190                        this.getClass().getClassLoader(),
191                        "lang/messages_%s.json" // the path in our jar file
192                ),
193                TranslatableCaption.DEFAULT_NAMESPACE
194        );
195        // Load caption map
196        try {
197            this.loadCaptionMap();
198        } catch (final Exception e) {
199            LOGGER.error("Failed to load caption map", e);
200            LOGGER.error("Shutting down server to prevent further issues");
201            this.platform.shutdownServer();
202            throw new RuntimeException("Abort loading PlotSquared");
203        }
204
205        // Setup the global flag container
206        GlobalFlagContainer.setup();
207
208        try {
209            new ReflectionUtils(this.platform.serverNativePackage());
210            try {
211                URL logurl = PlotSquared.class.getProtectionDomain().getCodeSource().getLocation();
212                this.jarFile = new File(
213                        URI.create(
214                                logurl.toURI().toString().split("\\!")[0].replaceAll("jar:file", "file"))
215                                .getPath());
216            } catch (URISyntaxException | SecurityException e) {
217                e.printStackTrace();
218                this.jarFile = new File(this.platform.getDirectory().getParentFile(), "PlotSquared.jar");
219                if (!this.jarFile.exists()) {
220                    this.jarFile = new File(
221                            this.platform.getDirectory().getParentFile(),
222                            "PlotSquared-" + platform + ".jar"
223                    );
224                }
225            }
226
227            this.worldedit = WorldEdit.getInstance();
228            WorldEdit.getInstance().getEventBus().register(new WEPlatformReadyListener());
229
230            // Create Event utility class
231            this.eventDispatcher = new EventDispatcher(this.worldedit);
232            // Create plot listener
233            this.plotListener = new PlotListener(this.eventDispatcher);
234
235            // Copy files
236            copyFile("town.template", Settings.Paths.TEMPLATES);
237            copyFile("bridge.template", Settings.Paths.TEMPLATES);
238            copyFile("skyblock.template", Settings.Paths.TEMPLATES);
239            showDebug();
240        } catch (Throwable e) {
241            e.printStackTrace();
242        }
243    }
244
245    /**
246     * Gets an instance of PlotSquared.
247     *
248     * @return instance of PlotSquared
249     */
250    public static @NonNull PlotSquared get() {
251        return PlotSquared.instance;
252    }
253
254    /**
255     * Get the platform specific implementation of PlotSquared
256     *
257     * @return Platform implementation
258     */
259    public static @NonNull PlotPlatform<?> platform() {
260        if (instance != null && instance.platform != null) {
261            return instance.platform;
262        }
263        throw new IllegalStateException("Plot platform implementation is missing");
264    }
265
266    public void loadCaptionMap() throws Exception {
267        this.platform.copyCaptionMaps();
268        // Setup localization
269        CaptionMap captionMap;
270        if (Settings.Enabled_Components.PER_USER_LOCALE) {
271            captionMap = this.captionLoader.loadAll(this.platform.getDirectory().toPath().resolve("lang"));
272        } else {
273            String fileName = "messages_" + Settings.Enabled_Components.DEFAULT_LOCALE + ".json";
274            captionMap = this.captionLoader.loadOrCreateSingle(this.platform
275                    .getDirectory()
276                    .toPath()
277                    .resolve("lang")
278                    .resolve(fileName));
279        }
280        this.captionMaps.put(TranslatableCaption.DEFAULT_NAMESPACE, captionMap);
281        LOGGER.info(
282                "Loaded caption map for namespace 'plotsquared': {}",
283                this.captionMaps.get(TranslatableCaption.DEFAULT_NAMESPACE).getClass().getCanonicalName()
284        );
285    }
286
287    /**
288     * Get the platform specific {@link PlotAreaManager} instance
289     *
290     * @return Plot area manager
291     */
292    public @NonNull PlotAreaManager getPlotAreaManager() {
293        return this.platform.plotAreaManager();
294    }
295
296    public void startExpiryTasks() {
297        if (Settings.Enabled_Components.PLOT_EXPIRY) {
298            ExpireManager expireManager = PlotSquared.platform().expireManager();
299            expireManager.runAutomatedTask();
300            for (Settings.Auto_Clear settings : Settings.AUTO_CLEAR.getInstances()) {
301                ExpiryTask task = new ExpiryTask(settings, this.getPlotAreaManager());
302                expireManager.addTask(task);
303            }
304        }
305    }
306
307    public boolean isMainThread(final @NonNull Thread thread) {
308        return this.thread == thread;
309    }
310
311    /**
312     * Check if `version` is &gt;= `version2`.
313     *
314     * @param version  First version
315     * @param version2 Second version
316     * @return {@code true} if `version` is &gt;= `version2`
317     */
318    public boolean checkVersion(
319            final int[] version,
320            final int... version2
321    ) {
322        return version[0] > version2[0] || version[0] == version2[0] && version[1] > version2[1]
323                || version[0] == version2[0] && version[1] == version2[1] && version[2] >= version2[2];
324    }
325
326    /**
327     * Gets the current PlotSquared version.
328     *
329     * @return current version in config or null
330     */
331    public @NonNull PlotVersion getVersion() {
332        return this.version;
333    }
334
335    /**
336     * Gets the server platform this plugin is running on this is running on.
337     *
338     * <p>This will be either <b>Bukkit</b> or <b>Sponge</b></p>
339     *
340     * @return the server implementation
341     */
342    public @NonNull String getPlatform() {
343        return Settings.PLATFORM;
344    }
345
346    /**
347     * Add a global reference to a plot world.
348     * <p>
349     * You can remove the reference by calling {@link #removePlotArea(PlotArea)}
350     * </p>
351     *
352     * @param plotArea the {@link PlotArea} to add.
353     */
354    @SuppressWarnings("unchecked")
355    public void addPlotArea(final @NonNull PlotArea plotArea) {
356        HashMap<PlotId, Plot> plots;
357        if (plots_tmp == null || (plots = plots_tmp.remove(plotArea.toString())) == null) {
358            if (plotArea.getType() == PlotAreaType.PARTIAL) {
359                plots = this.plots_tmp != null ? this.plots_tmp.get(plotArea.getWorldName()) : null;
360                if (plots != null) {
361                    Iterator<Entry<PlotId, Plot>> iterator = plots.entrySet().iterator();
362                    while (iterator.hasNext()) {
363                        Entry<PlotId, Plot> next = iterator.next();
364                        PlotId id = next.getKey();
365                        if (plotArea.contains(id)) {
366                            next.getValue().setArea(plotArea);
367                            iterator.remove();
368                        }
369                    }
370                }
371            }
372        } else {
373            for (Plot entry : plots.values()) {
374                entry.setArea(plotArea);
375            }
376        }
377        Set<PlotCluster> clusters;
378        if (clustersTmp == null || (clusters = clustersTmp.remove(plotArea.toString())) == null) {
379            if (plotArea.getType() == PlotAreaType.PARTIAL) {
380                clusters = this.clustersTmp != null ?
381                        this.clustersTmp.get(plotArea.getWorldName()) :
382                        null;
383                if (clusters != null) {
384                    Iterator<PlotCluster> iterator = clusters.iterator();
385                    while (iterator.hasNext()) {
386                        PlotCluster next = iterator.next();
387                        if (next.intersects(plotArea.getMin(), plotArea.getMax())) {
388                            next.setArea(plotArea);
389                            iterator.remove();
390                        }
391                    }
392                }
393            }
394        } else {
395            for (PlotCluster cluster : clusters) {
396                cluster.setArea(plotArea);
397            }
398        }
399        getPlotAreaManager().addPlotArea(plotArea);
400        plotArea.setupBorder();
401        if (!Settings.Enabled_Components.PERSISTENT_ROAD_REGEN) {
402            return;
403        }
404        File file = new File(
405                this.platform.getDirectory() + File.separator + "persistent_regen_data_" + plotArea.getId()
406                        + "_" + plotArea.getWorldName());
407        if (!file.exists()) {
408            return;
409        }
410        TaskManager.runTaskAsync(() -> {
411            try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file))) {
412                List<Object> list = (List<Object>) ois.readObject();
413                ArrayList<int[]> regionInts = (ArrayList<int[]>) list.get(0);
414                ArrayList<int[]> chunkInts = (ArrayList<int[]>) list.get(1);
415                HashSet<BlockVector2> regions = new HashSet<>();
416                Set<BlockVector2> chunks = new HashSet<>();
417                regionInts.forEach(l -> regions.add(BlockVector2.at(l[0], l[1])));
418                chunkInts.forEach(l -> chunks.add(BlockVector2.at(l[0], l[1])));
419                int height = (int) list.get(2);
420                LOGGER.info(
421                        "Incomplete road regeneration found. Restarting in world {} with height {}",
422                        plotArea.getWorldName(),
423                        height
424                );
425                LOGGER.info("- Regions: {}", regions.size());
426                LOGGER.info("- Chunks: {}", chunks.size());
427                HybridUtils.UPDATE = true;
428                PlotSquared.platform().hybridUtils().scheduleRoadUpdate(plotArea, regions, height, chunks);
429            } catch (IOException | ClassNotFoundException e) {
430                LOGGER.error("Error restarting road regeneration", e);
431            } finally {
432                if (!file.delete()) {
433                    LOGGER.error("Error deleting persistent_regen_data_{}. Please delete this file manually", plotArea.getId());
434                }
435            }
436        });
437    }
438
439    /**
440     * Remove a plot world reference.
441     *
442     * @param area the {@link PlotArea} to remove
443     */
444    public void removePlotArea(final @NonNull PlotArea area) {
445        getPlotAreaManager().removePlotArea(area);
446        setPlotsTmp(area);
447    }
448
449    public void removePlotAreas(final @NonNull String world) {
450        for (final PlotArea area : this.getPlotAreaManager().getPlotAreasSet(world)) {
451            if (area.getWorldName().equals(world)) {
452                removePlotArea(area);
453            }
454        }
455    }
456
457    private void setPlotsTmp(final @NonNull PlotArea area) {
458        if (this.plots_tmp == null) {
459            this.plots_tmp = new HashMap<>();
460        }
461        HashMap<PlotId, Plot> map =
462                this.plots_tmp.computeIfAbsent(area.toString(), k -> new HashMap<>());
463        for (Plot plot : area.getPlots()) {
464            map.put(plot.getId(), plot);
465        }
466        if (this.clustersTmp == null) {
467            this.clustersTmp = new HashMap<>();
468        }
469        this.clustersTmp.put(area.toString(), area.getClusters());
470    }
471
472    public Set<PlotCluster> getClusters(final @NonNull String world) {
473        final Set<PlotCluster> set = new HashSet<>();
474        for (final PlotArea area : this.getPlotAreaManager().getPlotAreasSet(world)) {
475            set.addAll(area.getClusters());
476        }
477        return Collections.unmodifiableSet(set);
478
479    }
480
481    public List<Plot> sortPlotsByTemp(Collection<Plot> plots) {
482        int max = 0;
483        int overflowCount = 0;
484        for (Plot plot : plots) {
485            if (plot.temp > 0) {
486                if (plot.temp > max) {
487                    max = plot.temp;
488                }
489            } else {
490                overflowCount++;
491            }
492        }
493        Plot[] array = new Plot[max + 1];
494        List<Plot> overflow = new ArrayList<>(overflowCount);
495        for (Plot plot : plots) {
496            if (plot.temp <= 0) {
497                overflow.add(plot);
498            } else {
499                array[plot.temp] = plot;
500            }
501        }
502        ArrayList<Plot> result = new ArrayList<>(plots.size());
503        for (Plot plot : array) {
504            if (plot != null) {
505                result.add(plot);
506            }
507        }
508        overflow.sort(Comparator.comparingInt(Plot::hashCode));
509        result.addAll(overflow);
510        return result;
511    }
512
513    /**
514     * Sort plots by hashcode.
515     *
516     * @param plots the collection of plots to sort
517     * @return the sorted collection
518     */
519    private ArrayList<Plot> sortPlotsByHash(Collection<Plot> plots) {
520        int hardmax = 256000;
521        int max = 0;
522        int overflowSize = 0;
523        for (Plot plot : plots) {
524            int hash = MathMan.getPositiveId(plot.hashCode());
525            if (hash > max) {
526                if (hash >= hardmax) {
527                    overflowSize++;
528                } else {
529                    max = hash;
530                }
531            }
532        }
533        hardmax = Math.min(hardmax, max);
534        Plot[] cache = new Plot[hardmax + 1];
535        List<Plot> overflow = new ArrayList<>(overflowSize);
536        ArrayList<Plot> extra = new ArrayList<>();
537        for (Plot plot : plots) {
538            int hash = MathMan.getPositiveId(plot.hashCode());
539            if (hash < hardmax) {
540                if (hash >= 0) {
541                    cache[hash] = plot;
542                } else {
543                    extra.add(plot);
544                }
545            } else if (Math.abs(plot.getId().getX()) > 15446 || Math.abs(plot.getId().getY()) > 15446) {
546                extra.add(plot);
547            } else {
548                overflow.add(plot);
549            }
550        }
551        Plot[] overflowArray = overflow.toArray(new Plot[0]);
552        sortPlotsByHash(overflowArray);
553        ArrayList<Plot> result = new ArrayList<>(cache.length + overflowArray.length);
554        for (Plot plot : cache) {
555            if (plot != null) {
556                result.add(plot);
557            }
558        }
559        Collections.addAll(result, overflowArray);
560        result.addAll(extra);
561        return result;
562    }
563
564    /**
565     * Unchecked, use {@link #sortPlots(Collection, SortType, PlotArea)} instead which will in turn call this.
566     *
567     * @param input an array of plots to sort
568     */
569    @SuppressWarnings("unchecked")
570    private void sortPlotsByHash(final @NonNull Plot @NonNull [] input) {
571        List<Plot>[] bucket = new ArrayList[32];
572        Arrays.fill(bucket, new ArrayList<>());
573        boolean maxLength = false;
574        int placement = 1;
575        while (!maxLength) {
576            maxLength = true;
577            for (Plot plot : input) {
578                int tmp = MathMan.getPositiveId(plot.hashCode()) / placement;
579                bucket[tmp & 31].add(plot);
580                if (maxLength && tmp > 0) {
581                    maxLength = false;
582                }
583            }
584            int a = 0;
585            for (int i = 0; i < 32; i++) {
586                for (Plot plot : bucket[i]) {
587                    input[a++] = plot;
588                }
589                bucket[i].clear();
590            }
591            placement *= 32;
592        }
593    }
594
595    private @NonNull List<Plot> sortPlotsByTimestamp(final @NonNull Collection<Plot> plots) {
596        int hardMax = 256000;
597        int max = 0;
598        int overflowSize = 0;
599        for (final Plot plot : plots) {
600            int hash = MathMan.getPositiveId(plot.hashCode());
601            if (hash > max) {
602                if (hash >= hardMax) {
603                    overflowSize++;
604                } else {
605                    max = hash;
606                }
607            }
608        }
609        hardMax = Math.min(hardMax, max);
610        Plot[] cache = new Plot[hardMax + 1];
611        List<Plot> overflow = new ArrayList<>(overflowSize);
612        ArrayList<Plot> extra = new ArrayList<>();
613        for (Plot plot : plots) {
614            int hash = MathMan.getPositiveId(plot.hashCode());
615            if (hash < hardMax) {
616                if (hash >= 0) {
617                    cache[hash] = plot;
618                } else {
619                    extra.add(plot);
620                }
621            } else if (Math.abs(plot.getId().getX()) > 15446 || Math.abs(plot.getId().getY()) > 15446) {
622                extra.add(plot);
623            } else {
624                overflow.add(plot);
625            }
626        }
627        Plot[] overflowArray = overflow.toArray(new Plot[0]);
628        sortPlotsByHash(overflowArray);
629        ArrayList<Plot> result = new ArrayList<>(cache.length + overflowArray.length);
630        for (Plot plot : cache) {
631            if (plot != null) {
632                result.add(plot);
633            }
634        }
635        Collections.addAll(result, overflowArray);
636        result.addAll(extra);
637        return result;
638    }
639
640    /**
641     * Sort plots by creation timestamp.
642     *
643     * @param input Plots to sort
644     * @return Sorted list
645     */
646    private @NonNull List<Plot> sortPlotsByModified(final @NonNull Collection<Plot> input) {
647        List<Plot> list;
648        if (input instanceof List) {
649            list = (List<Plot>) input;
650        } else {
651            list = new ArrayList<>(input);
652        }
653        ExpireManager expireManager = PlotSquared.platform().expireManager();
654        list.sort(Comparator.comparingLong(a -> expireManager.getTimestamp(a.getOwnerAbs())));
655        return list;
656    }
657
658    /**
659     * Sort a collection of plots by world (with a priority world), then
660     * by hashcode.
661     *
662     * @param plots        the plots to sort
663     * @param type         The sorting method to use for each world (timestamp, or hash)
664     * @param priorityArea Use null, "world", or "gibberish" if you
665     *                     want default world order
666     * @return ArrayList of plot
667     */
668    public @NonNull List<Plot> sortPlots(
669            final @NonNull Collection<Plot> plots,
670            final @NonNull SortType type,
671            final @Nullable PlotArea priorityArea
672    ) {
673        // group by world
674        // sort each
675        HashMap<PlotArea, Collection<Plot>> map = new HashMap<>();
676        int totalSize = Arrays.stream(this.getPlotAreaManager().getAllPlotAreas()).mapToInt(PlotArea::getPlotCount).sum();
677        if (plots.size() == totalSize) {
678            for (PlotArea area : getPlotAreaManager().getAllPlotAreas()) {
679                map.put(area, area.getPlots());
680            }
681        } else {
682            for (PlotArea area : getPlotAreaManager().getAllPlotAreas()) {
683                map.put(area, new ArrayList<>(0));
684            }
685            Collection<Plot> lastList = null;
686            PlotArea lastWorld = null;
687            for (Plot plot : plots) {
688                if (lastWorld == plot.getArea()) {
689                    lastList.add(plot);
690                } else {
691                    lastWorld = plot.getArea();
692                    lastList = map.get(lastWorld);
693                    lastList.add(plot);
694                }
695            }
696        }
697        List<PlotArea> areas = Arrays.asList(getPlotAreaManager().getAllPlotAreas());
698        areas.sort((a, b) -> {
699            if (priorityArea != null) {
700                if (a.equals(priorityArea)) {
701                    return -1;
702                } else if (b.equals(priorityArea)) {
703                    return 1;
704                }
705            }
706            return a.hashCode() - b.hashCode();
707        });
708        ArrayList<Plot> toReturn = new ArrayList<>(plots.size());
709        for (PlotArea area : areas) {
710            switch (type) {
711                case CREATION_DATE -> toReturn.addAll(sortPlotsByTemp(map.get(area)));
712                case CREATION_DATE_TIMESTAMP -> toReturn.addAll(sortPlotsByTimestamp(map.get(area)));
713                case DISTANCE_FROM_ORIGIN -> toReturn.addAll(sortPlotsByHash(map.get(area)));
714                case LAST_MODIFIED -> toReturn.addAll(sortPlotsByModified(map.get(area)));
715                default -> {
716                }
717            }
718        }
719        return toReturn;
720    }
721
722    public void setPlots(final @NonNull Map<String, HashMap<PlotId, Plot>> plots) {
723        if (this.plots_tmp == null) {
724            this.plots_tmp = new HashMap<>();
725        }
726        for (final Entry<String, HashMap<PlotId, Plot>> entry : plots.entrySet()) {
727            final String world = entry.getKey();
728            final PlotArea plotArea = this.getPlotAreaManager().getPlotArea(world, null);
729            if (plotArea == null) {
730                Map<PlotId, Plot> map = this.plots_tmp.computeIfAbsent(world, k -> new HashMap<>());
731                map.putAll(entry.getValue());
732            } else {
733                for (Plot plot : entry.getValue().values()) {
734                    plot.setArea(plotArea);
735                    plotArea.addPlot(plot);
736                }
737            }
738        }
739    }
740
741    /**
742     * Unregisters a plot from local memory without calling the database.
743     *
744     * @param plot      the plot to remove
745     * @param callEvent If to call an event about the plot being removed
746     * @return {@code true} if plot existed | {@code false} if it didn't
747     */
748    public boolean removePlot(
749            final @NonNull Plot plot,
750            final boolean callEvent
751    ) {
752        if (plot == null) {
753            return false;
754        }
755        if (callEvent) {
756            eventDispatcher.callDelete(plot);
757        }
758        if (plot.getArea().removePlot(plot.getId())) {
759            PlotId last = (PlotId) plot.getArea().getMeta("lastPlot");
760            int last_max = Math.max(Math.abs(last.getX()), Math.abs(last.getY()));
761            int this_max = Math.max(Math.abs(plot.getId().getX()), Math.abs(plot.getId().getY()));
762            if (this_max < last_max) {
763                plot.getArea().setMeta("lastPlot", plot.getId());
764            }
765            if (callEvent) {
766                eventDispatcher.callPostDelete(plot);
767            }
768            return true;
769        }
770        return false;
771    }
772
773    /**
774     * This method is called by the PlotGenerator class normally.
775     * <ul>
776     * <li>Initializes the PlotArea and PlotManager classes
777     * <li>Registers the PlotArea and PlotManager classes
778     * <li>Loads (and/or generates) the PlotArea configuration
779     * <li>Sets up the world border if configured
780     * </ul>
781     *
782     * <p>If loading an augmented plot world:
783     * <ul>
784     * <li>Creates the AugmentedPopulator classes
785     * <li>Injects the AugmentedPopulator classes if required
786     * </ul>
787     *
788     * @param world         the world to load
789     * @param baseGenerator The generator for that world, or null
790     */
791    public void loadWorld(
792            final @NonNull String world,
793            final @Nullable GeneratorWrapper<?> baseGenerator
794    ) {
795        if (world.equals("CheckingPlotSquaredGenerator")) {
796            return;
797        }
798        // Don't check the return result -> breaks runtime loading of single plot areas on creation
799        this.getPlotAreaManager().addWorld(world);
800        Set<String> worlds;
801        if (this.worldConfiguration.contains("worlds")) {
802            worlds = this.worldConfiguration.getConfigurationSection("worlds").getKeys(false);
803        } else {
804            worlds = new HashSet<>();
805        }
806        String path = "worlds." + world;
807        ConfigurationSection worldSection = this.worldConfiguration.getConfigurationSection(path);
808        PlotAreaType type;
809        if (worldSection != null) {
810            type = ConfigurationUtil.getType(worldSection);
811        } else {
812            type = PlotAreaType.NORMAL;
813        }
814        if (type == PlotAreaType.NORMAL) {
815            if (getPlotAreaManager().getPlotAreas(world, null).length != 0) {
816                return;
817            }
818            IndependentPlotGenerator plotGenerator;
819            if (baseGenerator != null && baseGenerator.isFull()) {
820                plotGenerator = baseGenerator.getPlotGenerator();
821            } else if (worldSection != null) {
822                String secondaryGeneratorName = worldSection.getString("generator.plugin");
823                GeneratorWrapper<?> secondaryGenerator =
824                        this.platform.getGenerator(world, secondaryGeneratorName);
825                if (secondaryGenerator != null && secondaryGenerator.isFull()) {
826                    plotGenerator = secondaryGenerator.getPlotGenerator();
827                } else {
828                    String primaryGeneratorName = worldSection.getString("generator.init");
829                    GeneratorWrapper<?> primaryGenerator =
830                            this.platform.getGenerator(world, primaryGeneratorName);
831                    if (primaryGenerator != null && primaryGenerator.isFull()) {
832                        plotGenerator = primaryGenerator.getPlotGenerator();
833                    } else {
834                        return;
835                    }
836                }
837            } else {
838                return;
839            }
840            // Conventional plot generator
841            PlotArea plotArea = plotGenerator.getNewPlotArea(world, null, null, null);
842            PlotManager plotManager = plotArea.getPlotManager();
843            LOGGER.info("Detected world load for '{}'", world);
844            LOGGER.info("- generator: {}>{}", baseGenerator, plotGenerator);
845            LOGGER.info("- plot world: {}", plotArea.getClass().getCanonicalName());
846            LOGGER.info("- plot area manager: {}", plotManager.getClass().getCanonicalName());
847            if (!this.worldConfiguration.contains(path)) {
848                this.worldConfiguration.createSection(path);
849                worldSection = this.worldConfiguration.getConfigurationSection(path);
850            }
851            plotArea.saveConfiguration(worldSection);
852            plotArea.loadDefaultConfiguration(worldSection);
853            try {
854                this.worldConfiguration.save(this.worldsFile);
855            } catch (IOException e) {
856                e.printStackTrace();
857            }
858            // Now add it
859            addPlotArea(plotArea);
860            plotGenerator.initialize(plotArea);
861        } else {
862            if (!worlds.contains(world)) {
863                return;
864            }
865            ConfigurationSection areasSection = worldSection.getConfigurationSection("areas");
866            if (areasSection == null) {
867                if (getPlotAreaManager().getPlotAreas(world, null).length != 0) {
868                    return;
869                }
870                LOGGER.info("Detected world load for '{}'", world);
871                String gen_string = worldSection.getString("generator.plugin", platform.pluginName());
872                if (type == PlotAreaType.PARTIAL) {
873                    Set<PlotCluster> clusters =
874                            this.clustersTmp != null ? this.clustersTmp.get(world) : new HashSet<>();
875                    if (clusters == null) {
876                        throw new IllegalArgumentException("No cluster exists for world: " + world);
877                    }
878                    ArrayDeque<PlotArea> toLoad = new ArrayDeque<>();
879                    for (PlotCluster cluster : clusters) {
880                        PlotId pos1 = cluster.getP1(); // Cluster pos1
881                        PlotId pos2 = cluster.getP2(); // Cluster pos2
882                        String name = cluster.getName(); // Cluster name
883                        String fullId = name + "-" + pos1 + "-" + pos2;
884                        worldSection.createSection("areas." + fullId);
885                        DBFunc.replaceWorld(world, world + ";" + name, pos1, pos2); // NPE
886                        LOGGER.info("- {}-{}-{}", name, pos1, pos2);
887                        GeneratorWrapper<?> areaGen = this.platform.getGenerator(world, gen_string);
888                        if (areaGen == null) {
889                            throw new IllegalArgumentException("Invalid Generator: " + gen_string);
890                        }
891                        PlotArea pa =
892                                areaGen.getPlotGenerator().getNewPlotArea(world, name, pos1, pos2);
893                        pa.saveConfiguration(worldSection);
894                        pa.loadDefaultConfiguration(worldSection);
895                        try {
896                            this.worldConfiguration.save(this.worldsFile);
897                        } catch (IOException e) {
898                            e.printStackTrace();
899                        }
900                        LOGGER.info("| generator: {}>{}", baseGenerator, areaGen);
901                        LOGGER.info("| plot world: {}", pa.getClass().getCanonicalName());
902                        LOGGER.info("| manager: {}", pa.getPlotManager().getClass().getCanonicalName());
903                        LOGGER.info("Note: Area created for cluster '{}' (invalid or old configuration?)", name);
904                        areaGen.getPlotGenerator().initialize(pa);
905                        areaGen.augment(pa);
906                        toLoad.add(pa);
907                    }
908                    for (PlotArea area : toLoad) {
909                        addPlotArea(area);
910                    }
911                    return;
912                }
913                GeneratorWrapper<?> areaGen = this.platform.getGenerator(world, gen_string);
914                if (areaGen == null) {
915                    throw new IllegalArgumentException("Invalid Generator: " + gen_string);
916                }
917                PlotArea pa = areaGen.getPlotGenerator().getNewPlotArea(world, null, null, null);
918                LOGGER.info("- generator: {}>{}", baseGenerator, areaGen);
919                LOGGER.info("- plot world: {}", pa.getClass().getCanonicalName());
920                LOGGER.info("- plot area manager: {}", pa.getPlotManager().getClass().getCanonicalName());
921                if (!this.worldConfiguration.contains(path)) {
922                    this.worldConfiguration.createSection(path);
923                    worldSection = this.worldConfiguration.getConfigurationSection(path);
924                }
925                pa.saveConfiguration(worldSection);
926                pa.loadDefaultConfiguration(worldSection);
927                try {
928                    this.worldConfiguration.save(this.worldsFile);
929                } catch (IOException e) {
930                    e.printStackTrace();
931                }
932                areaGen.getPlotGenerator().initialize(pa);
933                areaGen.augment(pa);
934                addPlotArea(pa);
935                return;
936            }
937            if (type == PlotAreaType.AUGMENTED) {
938                throw new IllegalArgumentException(
939                        "Invalid type for multi-area world. Expected `PARTIAL`, got `"
940                                + PlotAreaType.AUGMENTED + "`");
941            }
942            for (String areaId : areasSection.getKeys(false)) {
943                LOGGER.info("- {}", areaId);
944                String[] split = areaId.split("(?<=[^;-])-");
945                if (split.length != 3) {
946                    throw new IllegalArgumentException("Invalid Area identifier: " + areaId
947                            + ". Expected form `<name>-<pos1>-<pos2>`");
948                }
949                String name = split[0];
950                PlotId pos1 = PlotId.fromString(split[1]);
951                PlotId pos2 = PlotId.fromString(split[2]);
952                if (name.isEmpty()) {
953                    throw new IllegalArgumentException("Invalid Area identifier: " + areaId
954                            + ". Expected form `<name>-<x1;z1>-<x2;z2>`");
955                }
956                final PlotArea existing = this.getPlotAreaManager().getPlotArea(world, name);
957                if (existing != null && name.equals(existing.getId())) {
958                    continue;
959                }
960                ConfigurationSection section = areasSection.getConfigurationSection(areaId);
961                YamlConfiguration clone = new YamlConfiguration();
962                for (String key : section.getKeys(true)) {
963                    if (section.get(key) instanceof MemorySection) {
964                        continue;
965                    }
966                    if (!clone.contains(key)) {
967                        clone.set(key, section.get(key));
968                    }
969                }
970                for (String key : worldSection.getKeys(true)) {
971                    if (worldSection.get(key) instanceof MemorySection) {
972                        continue;
973                    }
974                    if (!key.startsWith("areas") && !clone.contains(key)) {
975                        clone.set(key, worldSection.get(key));
976                    }
977                }
978                String gen_string = clone.getString("generator.plugin", platform.pluginName());
979                GeneratorWrapper<?> areaGen = this.platform.getGenerator(world, gen_string);
980                if (areaGen == null) {
981                    throw new IllegalArgumentException("Invalid Generator: " + gen_string);
982                }
983                PlotArea pa = areaGen.getPlotGenerator().getNewPlotArea(world, name, pos1, pos2);
984                pa.saveConfiguration(clone);
985                // netSections is the combination of
986                for (String key : clone.getKeys(true)) {
987                    if (clone.get(key) instanceof MemorySection) {
988                        continue;
989                    }
990                    if (!worldSection.contains(key)) {
991                        worldSection.set(key, clone.get(key));
992                    } else {
993                        Object value = worldSection.get(key);
994                        if (!Objects.equals(value, clone.get(key))) {
995                            section.set(key, clone.get(key));
996                        }
997                    }
998                }
999                pa.loadDefaultConfiguration(clone);
1000                try {
1001                    this.worldConfiguration.save(this.worldsFile);
1002                } catch (IOException e) {
1003                    e.printStackTrace();
1004                }
1005                LOGGER.info("Detected area load for '{}'", world);
1006                LOGGER.info("| generator: {}>{}", baseGenerator, areaGen);
1007                LOGGER.info("| plot world: {}", pa);
1008                LOGGER.info("| manager: {}", pa.getPlotManager());
1009                areaGen.getPlotGenerator().initialize(pa);
1010                areaGen.augment(pa);
1011                addPlotArea(pa);
1012            }
1013        }
1014    }
1015
1016    /**
1017     * Setup the configuration for a plot world based on world arguments.
1018     * <p>
1019     *
1020     * <i>e.g. /mv create &lt;world&gt; normal -g PlotSquared:&lt;args&gt;</i>
1021     *
1022     * @param world     The name of the world
1023     * @param args      The arguments
1024     * @param generator the plot generator
1025     * @return boolean | if valid arguments were provided
1026     */
1027    public boolean setupPlotWorld(
1028            final @NonNull String world,
1029            final @Nullable String args,
1030            final @NonNull IndependentPlotGenerator generator
1031    ) {
1032        if (args != null && !args.isEmpty()) {
1033            // save configuration
1034
1035            final List<String> validArguments = Arrays
1036                    .asList("s=", "size=", "g=", "gap=", "h=", "height=", "minh=", "minheight=", "maxh=", "maxheight=",
1037                            "f=", "floor=", "m=", "main=", "w=", "wall=", "b=", "border="
1038                    );
1039
1040            // Calculate the number of expected arguments
1041            int expected = (int) validArguments.stream()
1042                    .filter(validArgument -> args.toLowerCase(Locale.ENGLISH).contains(validArgument))
1043                    .count();
1044
1045            String[] split = args.toLowerCase(Locale.ENGLISH).split(",(?![^\\(\\[]*[\\]\\)])");
1046
1047            if (split.length > expected) {
1048                // This means we have multi-block block buckets
1049                String[] combinedArgs = new String[expected];
1050                int index = 0;
1051
1052                StringBuilder argBuilder = new StringBuilder();
1053                outer:
1054                for (final String string : split) {
1055                    for (final String validArgument : validArguments) {
1056                        if (string.contains(validArgument)) {
1057                            if (!argBuilder.toString().isEmpty()) {
1058                                combinedArgs[index++] = argBuilder.toString();
1059                                argBuilder = new StringBuilder();
1060                            }
1061                            argBuilder.append(string);
1062                            continue outer;
1063                        }
1064                    }
1065                    if (argBuilder.toString().charAt(argBuilder.length() - 1) != '=') {
1066                        argBuilder.append(",");
1067                    }
1068                    argBuilder.append(string);
1069                }
1070
1071                if (!argBuilder.toString().isEmpty()) {
1072                    combinedArgs[index] = argBuilder.toString();
1073                }
1074
1075                split = combinedArgs;
1076            }
1077
1078            final HybridPlotWorldFactory hybridPlotWorldFactory = this.platform
1079                    .injector()
1080                    .getInstance(HybridPlotWorldFactory.class);
1081            final HybridPlotWorld plotWorld = hybridPlotWorldFactory.create(world, null, generator, null, null);
1082
1083            for (String element : split) {
1084                String[] pair = element.split("=");
1085                if (pair.length != 2) {
1086                    LOGGER.error("No value provided for '{}'", element);
1087                    return false;
1088                }
1089                String key = pair[0].toLowerCase();
1090                String value = pair[1];
1091                try {
1092                    String base = "worlds." + world + ".";
1093                    switch (key) {
1094                        case "s", "size" -> this.worldConfiguration.set(
1095                                base + "plot.size",
1096                                ConfigurationUtil.INTEGER.parseString(value).shortValue()
1097                        );
1098                        case "g", "gap" -> this.worldConfiguration.set(
1099                                base + "road.width",
1100                                ConfigurationUtil.INTEGER.parseString(value).shortValue()
1101                        );
1102                        case "h", "height" -> {
1103                            this.worldConfiguration.set(
1104                                    base + "road.height",
1105                                    ConfigurationUtil.INTEGER.parseString(value).shortValue()
1106                            );
1107                            this.worldConfiguration.set(
1108                                    base + "plot.height",
1109                                    ConfigurationUtil.INTEGER.parseString(value).shortValue()
1110                            );
1111                            this.worldConfiguration.set(
1112                                    base + "wall.height",
1113                                    ConfigurationUtil.INTEGER.parseString(value).shortValue()
1114                            );
1115                        }
1116                        case "minh", "minheight" -> this.worldConfiguration.set(
1117                                base + "world.min_gen_height",
1118                                ConfigurationUtil.INTEGER.parseString(value).shortValue()
1119                        );
1120                        case "maxh", "maxheight" -> this.worldConfiguration.set(
1121                                base + "world.max_gen_height",
1122                                ConfigurationUtil.INTEGER.parseString(value).shortValue()
1123                        );
1124                        case "f", "floor" -> this.worldConfiguration.set(
1125                                base + "plot.floor",
1126                                ConfigurationUtil.BLOCK_BUCKET.parseString(value).toString()
1127                        );
1128                        case "m", "main" -> this.worldConfiguration.set(
1129                                base + "plot.filling",
1130                                ConfigurationUtil.BLOCK_BUCKET.parseString(value).toString()
1131                        );
1132                        case "w", "wall" -> this.worldConfiguration.set(
1133                                base + "wall.filling",
1134                                ConfigurationUtil.BLOCK_BUCKET.parseString(value).toString()
1135                        );
1136                        case "b", "border" -> this.worldConfiguration.set(
1137                                base + "wall.block",
1138                                ConfigurationUtil.BLOCK_BUCKET.parseString(value).toString()
1139                        );
1140                        default -> {
1141                            LOGGER.error("Key not found: {}", element);
1142                            return false;
1143                        }
1144                    }
1145                } catch (Exception e) {
1146                    LOGGER.error("Invalid value '{}' for arg '{}'", value, element);
1147                    e.printStackTrace();
1148                    return false;
1149                }
1150            }
1151            try {
1152                ConfigurationSection section =
1153                        this.worldConfiguration.getConfigurationSection("worlds." + world);
1154                plotWorld.saveConfiguration(section);
1155                plotWorld.loadDefaultConfiguration(section);
1156                this.worldConfiguration.save(this.worldsFile);
1157            } catch (IOException e) {
1158                e.printStackTrace();
1159            }
1160        }
1161        return true;
1162    }
1163
1164    /**
1165     * Copies a file from inside the jar to a location
1166     *
1167     * @param file   Name of the file inside PlotSquared.jar
1168     * @param folder The output location relative to /plugins/PlotSquared/
1169     */
1170    public void copyFile(
1171            final @NonNull String file,
1172            final @NonNull String folder
1173    ) {
1174        try {
1175            File output = this.platform.getDirectory();
1176            if (!output.exists()) {
1177                output.mkdirs();
1178            }
1179            File newFile = FileUtils.getFile(output, folder + File.separator + file);
1180            if (newFile.exists()) {
1181                return;
1182            }
1183            try (InputStream stream = this.platform.getClass().getResourceAsStream(file)) {
1184                byte[] buffer = new byte[2048];
1185                if (stream == null) {
1186                    try (ZipInputStream zis = new ZipInputStream(
1187                            new FileInputStream(this.jarFile))) {
1188                        ZipEntry ze = zis.getNextEntry();
1189                        while (ze != null) {
1190                            String name = ze.getName();
1191                            if (name.equals(file)) {
1192                                new File(newFile.getParent()).mkdirs();
1193                                try (FileOutputStream fos = new FileOutputStream(newFile)) {
1194                                    int len;
1195                                    while ((len = zis.read(buffer)) > 0) {
1196                                        fos.write(buffer, 0, len);
1197                                    }
1198                                }
1199                                ze = null;
1200                            } else {
1201                                ze = zis.getNextEntry();
1202                            }
1203                        }
1204                        zis.closeEntry();
1205                    }
1206                    return;
1207                }
1208                newFile.createNewFile();
1209                try (FileOutputStream fos = new FileOutputStream(newFile)) {
1210                    int len;
1211                    while ((len = stream.read(buffer)) > 0) {
1212                        fos.write(buffer, 0, len);
1213                    }
1214                }
1215            }
1216        } catch (IOException e) {
1217            LOGGER.error("Could not save {}", file);
1218            e.printStackTrace();
1219        }
1220    }
1221
1222    /**
1223     * Safely closes the database connection.
1224     */
1225    public void disable() {
1226        try {
1227            eventDispatcher.unregisterAll();
1228            checkRoadRegenPersistence();
1229            // Validate that all data in the db is correct
1230            final HashSet<Plot> plots = new HashSet<>();
1231            try {
1232                forEachPlotRaw(plots::add);
1233            } catch (final Exception ignored) {
1234            }
1235            DBFunc.validatePlots(plots);
1236
1237            // Close the connection
1238            DBFunc.close();
1239        } catch (NullPointerException throwable) {
1240            LOGGER.error("Could not close database connection", throwable);
1241            throwable.printStackTrace();
1242        }
1243    }
1244
1245    /**
1246     * Handle road regen persistence
1247     */
1248    private void checkRoadRegenPersistence() {
1249        if (!HybridUtils.UPDATE || !Settings.Enabled_Components.PERSISTENT_ROAD_REGEN || (
1250                HybridUtils.regions.isEmpty() && HybridUtils.chunks.isEmpty())) {
1251            return;
1252        }
1253        LOGGER.info("Road regeneration incomplete. Saving incomplete regions to disk");
1254        LOGGER.info("- regions: {}", HybridUtils.regions.size());
1255        LOGGER.info("- chunks: {}", HybridUtils.chunks.size());
1256        ArrayList<int[]> regions = new ArrayList<>();
1257        ArrayList<int[]> chunks = new ArrayList<>();
1258        for (BlockVector2 r : HybridUtils.regions) {
1259            regions.add(new int[]{r.getBlockX(), r.getBlockZ()});
1260        }
1261        for (BlockVector2 c : HybridUtils.chunks) {
1262            chunks.add(new int[]{c.getBlockX(), c.getBlockZ()});
1263        }
1264        List<Object> list = new ArrayList<>();
1265        list.add(regions);
1266        list.add(chunks);
1267        list.add(HybridUtils.height);
1268        File file = new File(
1269                this.platform.getDirectory() + File.separator + "persistent_regen_data_" + HybridUtils.area
1270                        .getId() + "_" + HybridUtils.area.getWorldName());
1271        if (file.exists() && !file.delete()) {
1272            LOGGER.error("persistent_regene_data file already exists and could not be deleted");
1273            return;
1274        }
1275        try (ObjectOutputStream oos = new ObjectOutputStream(
1276                Files.newOutputStream(file.toPath(), StandardOpenOption.CREATE_NEW))) {
1277            oos.writeObject(list);
1278        } catch (IOException e) {
1279            LOGGER.error("Error creating persistent_region_data file", e);
1280        }
1281    }
1282
1283    /**
1284     * Set up the database connection.
1285     */
1286    public void setupDatabase() {
1287        try {
1288            if (DBFunc.dbManager != null) {
1289                DBFunc.dbManager.close();
1290            }
1291            Database database;
1292            if (Storage.MySQL.USE) {
1293                database = new MySQL(Storage.MySQL.HOST, Storage.MySQL.PORT, Storage.MySQL.DATABASE,
1294                        Storage.MySQL.USER, Storage.MySQL.PASSWORD
1295                );
1296            } else if (Storage.SQLite.USE) {
1297                File file = FileUtils.getFile(platform.getDirectory(), Storage.SQLite.DB + ".db");
1298                database = new SQLite(file);
1299            } else {
1300                LOGGER.error("No storage type is set. Disabling PlotSquared");
1301                this.platform.shutdown(); //shutdown used instead of disable because no database is set
1302                return;
1303            }
1304            DBFunc.dbManager = new SQLManager(
1305                    database,
1306                    Storage.PREFIX,
1307                    this.eventDispatcher,
1308                    this.plotListener,
1309                    this.worldConfiguration
1310            );
1311            this.plots_tmp = DBFunc.getPlots();
1312            if (getPlotAreaManager() instanceof SinglePlotAreaManager) {
1313                SinglePlotArea area = ((SinglePlotAreaManager) getPlotAreaManager()).getArea();
1314                addPlotArea(area);
1315                ConfigurationSection section = worldConfiguration.getConfigurationSection("worlds.*");
1316                if (section == null) {
1317                    section = worldConfiguration.createSection("worlds.*");
1318                }
1319                area.saveConfiguration(section);
1320                area.loadDefaultConfiguration(section);
1321            }
1322            this.clustersTmp = DBFunc.getClusters();
1323            LOGGER.info("Connection to database established. Type: {}", Storage.MySQL.USE ? "MySQL" : "SQLite");
1324        } catch (ClassNotFoundException | SQLException e) {
1325            LOGGER.error(
1326                    "Failed to open database connection ({}). Disabling PlotSquared",
1327                    Storage.MySQL.USE ? "MySQL" : "SQLite"
1328            );
1329            LOGGER.error("==== Here is an ugly stacktrace, if you are interested in those things ===");
1330            e.printStackTrace();
1331            LOGGER.error("==== End of stacktrace ====");
1332            LOGGER.error(
1333                    "Please go to the {} 'storage.yml' and configure the database correctly",
1334                    platform.pluginName()
1335            );
1336            this.platform.shutdown(); //shutdown used instead of disable because of database error
1337        }
1338    }
1339
1340    /**
1341     * Setup the default configuration.
1342     */
1343    public void setupConfig() {
1344        String lastVersionString = this.getConfig().getString("version");
1345        if (lastVersionString != null) {
1346            String[] split = lastVersionString.split("\\.");
1347            int[] lastVersion = new int[]{Integer.parseInt(split[0]), Integer.parseInt(split[1]),
1348                    Integer.parseInt(split[2])};
1349            if (checkVersion(new int[]{3, 4, 0}, lastVersion)) {
1350                Settings.convertLegacy(configFile);
1351                if (getConfig().contains("worlds")) {
1352                    ConfigurationSection worldSection =
1353                            getConfig().getConfigurationSection("worlds");
1354                    worldConfiguration.set("worlds", worldSection);
1355                    try {
1356                        worldConfiguration.save(worldsFile);
1357                    } catch (IOException e) {
1358                        LOGGER.error("Failed to save worlds.yml", e);
1359                        e.printStackTrace();
1360                    }
1361                }
1362                Settings.save(configFile);
1363            }
1364        }
1365        Settings.load(configFile);
1366        //Sets the version information for the settings.yml file
1367        try (InputStream stream = getClass().getResourceAsStream("/plugin.properties")) {
1368            try (BufferedReader br = new BufferedReader(new InputStreamReader(stream))) {
1369                String versionString = br.readLine();
1370                String commitString = br.readLine();
1371                String dateString = br.readLine();
1372                this.version = PlotVersion.tryParse(versionString, commitString, dateString);
1373            }
1374        } catch (IOException throwable) {
1375            throwable.printStackTrace();
1376        }
1377        Settings.save(configFile);
1378        config = YamlConfiguration.loadConfiguration(configFile);
1379    }
1380
1381    /**
1382     * Setup all configuration files<br>
1383     * - Config: settings.yml<br>
1384     * - Storage: storage.yml<br>
1385     *
1386     * @return success or not
1387     */
1388    public boolean setupConfigs() {
1389        File folder = new File(this.platform.getDirectory(), "config");
1390        if (!folder.exists() && !folder.mkdirs()) {
1391            LOGGER.error("Failed to create the {} config folder. Please create it manually", this.platform.getDirectory());
1392        }
1393        try {
1394            this.worldsFile = new File(folder, "worlds.yml");
1395            if (!this.worldsFile.exists() && !this.worldsFile.createNewFile()) {
1396                LOGGER.error("Could not create the worlds file. Please create 'worlds.yml' manually");
1397            }
1398            this.worldConfiguration = YamlConfiguration.loadConfiguration(this.worldsFile);
1399
1400            if (this.worldConfiguration.contains("worlds")) {
1401                if (!this.worldConfiguration.contains("configuration_version") || (
1402                        !this.worldConfiguration.getString("configuration_version")
1403                                .equalsIgnoreCase(LegacyConverter.CONFIGURATION_VERSION) && !this.worldConfiguration
1404                                .getString("configuration_version").equalsIgnoreCase("v5"))) {
1405                    // Conversion needed
1406                    LOGGER.info("A legacy configuration file was detected. Conversion will be attempted.");
1407                    try {
1408                        com.google.common.io.Files
1409                                .copy(this.worldsFile, new File(folder, "worlds.yml.old"));
1410                        LOGGER.info("A copy of worlds.yml has been saved in the file worlds.yml.old");
1411                        final ConfigurationSection worlds =
1412                                this.worldConfiguration.getConfigurationSection("worlds");
1413                        final LegacyConverter converter = new LegacyConverter(worlds);
1414                        converter.convert();
1415                        this.worldConfiguration.set("worlds", worlds);
1416                        this.setConfigurationVersion(LegacyConverter.CONFIGURATION_VERSION);
1417                        LOGGER.info(
1418                                "The conversion has finished. PlotSquared will now be disabled and the new configuration file will be used at next startup. Please review the new worlds.yml file. Please note that schematics will not be converted, as we are now using WorldEdit to handle schematics. You need to re-generate the schematics.");
1419                    } catch (final Exception e) {
1420                        LOGGER.error("Failed to convert the legacy configuration file. See stack trace for information.", e);
1421                    }
1422                    // Disable plugin
1423                    this.platform.shutdown();
1424                    return false;
1425                }
1426            } else {
1427                this.worldConfiguration.set("configuration_version", LegacyConverter.CONFIGURATION_VERSION);
1428            }
1429        } catch (IOException ignored) {
1430            LOGGER.error("Failed to save worlds.yml");
1431        }
1432        try {
1433            this.configFile = new File(folder, "settings.yml");
1434            if (!this.configFile.exists() && !this.configFile.createNewFile()) {
1435                LOGGER.error("Could not create the settings file. Please create 'settings.yml' manually");
1436            }
1437            this.config = YamlConfiguration.loadConfiguration(this.configFile);
1438            setupConfig();
1439        } catch (IOException ignored) {
1440            LOGGER.error("Failed to save settings.yml");
1441        }
1442        try {
1443            this.storageFile = new File(folder, "storage.yml");
1444            if (!this.storageFile.exists() && !this.storageFile.createNewFile()) {
1445                LOGGER.error("Could not create the storage settings file. Please create 'storage.yml' manually");
1446            }
1447            YamlConfiguration.loadConfiguration(this.storageFile);
1448            setupStorage();
1449        } catch (IOException ignored) {
1450            LOGGER.error("Failed to save storage.yml");
1451        }
1452        return true;
1453    }
1454
1455    public @NonNull String getConfigurationVersion() {
1456        return this.worldConfiguration.get("configuration_version", LegacyConverter.CONFIGURATION_VERSION)
1457                .toString();
1458    }
1459
1460    public void setConfigurationVersion(final @NonNull String newVersion) throws IOException {
1461        this.worldConfiguration.set("configuration_version", newVersion);
1462        this.worldConfiguration.save(this.worldsFile);
1463    }
1464
1465    /**
1466     * Setup the storage file (load + save missing nodes).
1467     */
1468    private void setupStorage() {
1469        Storage.load(storageFile);
1470        Storage.save(storageFile);
1471        YamlConfiguration.loadConfiguration(storageFile);
1472    }
1473
1474    /**
1475     * Show startup debug information.
1476     */
1477    private void showDebug() {
1478        if (Settings.DEBUG) {
1479            Map<String, Object> components = Settings.getFields(Settings.Enabled_Components.class);
1480            for (Entry<String, Object> component : components.entrySet()) {
1481                LOGGER.info("Key: {} | Value: {}", component.getKey(), component.getValue());
1482            }
1483        }
1484    }
1485
1486    public void forEachPlotRaw(final @NonNull Consumer<Plot> consumer) {
1487        for (final PlotArea area : this.getPlotAreaManager().getAllPlotAreas()) {
1488            area.getPlots().forEach(consumer);
1489        }
1490        if (this.plots_tmp != null) {
1491            for (final HashMap<PlotId, Plot> entry : this.plots_tmp.values()) {
1492                entry.values().forEach(consumer);
1493            }
1494        }
1495    }
1496
1497    /**
1498     * Check if the chunk uses vanilla/non-PlotSquared generation
1499     *
1500     * @param world            World name
1501     * @param chunkCoordinates Chunk coordinates
1502     * @return {@code true} if the chunk uses non-standard generation, {@code false} if not
1503     */
1504    public boolean isNonStandardGeneration(
1505            final @NonNull String world,
1506            final @NonNull BlockVector2 chunkCoordinates
1507    ) {
1508        final Location location = Location.at(world, chunkCoordinates.getBlockX() << 4, 64, chunkCoordinates.getBlockZ() << 4);
1509        final PlotArea area = getPlotAreaManager().getApplicablePlotArea(location);
1510        if (area == null) {
1511            return true;
1512        }
1513        return area.getTerrain() != PlotAreaTerrainType.NONE;
1514    }
1515
1516    public @NonNull YamlConfiguration getConfig() {
1517        return config;
1518    }
1519
1520    public @NonNull UUIDPipeline getImpromptuUUIDPipeline() {
1521        return this.impromptuUUIDPipeline;
1522    }
1523
1524    public @NonNull UUIDPipeline getBackgroundUUIDPipeline() {
1525        return this.backgroundUUIDPipeline;
1526    }
1527
1528    public @NonNull WorldEdit getWorldEdit() {
1529        return this.worldedit;
1530    }
1531
1532    public @NonNull File getConfigFile() {
1533        return this.configFile;
1534    }
1535
1536    public @NonNull File getWorldsFile() {
1537        return this.worldsFile;
1538    }
1539
1540    public @NonNull YamlConfiguration getWorldConfiguration() {
1541        return this.worldConfiguration;
1542    }
1543
1544    /**
1545     * Get the caption map belonging to a namespace. If none exists, a dummy
1546     * caption map will be returned.
1547     * <p>
1548     * You can register a caption map by calling {@link #registerCaptionMap(String, CaptionMap)}
1549     * </p>
1550     *
1551     * @param namespace Namespace
1552     * @return Map instance
1553     */
1554    public @NonNull CaptionMap getCaptionMap(final @NonNull String namespace) {
1555        return this.captionMaps.computeIfAbsent(
1556                namespace.toLowerCase(Locale.ENGLISH),
1557                missingNamespace -> new DummyCaptionMap()
1558        );
1559    }
1560
1561    /**
1562     * Register a caption map. The namespace needs to be equal to the namespace used for
1563     * the {@link TranslatableCaption}s inside the map.
1564     *
1565     * @param namespace  Namespace
1566     * @param captionMap Map instance
1567     */
1568    public void registerCaptionMap(
1569            final @NonNull String namespace,
1570            final @NonNull CaptionMap captionMap
1571    ) {
1572        if (namespace.equalsIgnoreCase(TranslatableCaption.DEFAULT_NAMESPACE)) {
1573            throw new IllegalArgumentException("Cannot replace default caption map");
1574        }
1575        this.captionMaps.put(namespace.toLowerCase(Locale.ENGLISH), captionMap);
1576    }
1577
1578    public @NonNull EventDispatcher getEventDispatcher() {
1579        return this.eventDispatcher;
1580    }
1581
1582    public @NonNull PlotListener getPlotListener() {
1583        return this.plotListener;
1584    }
1585
1586    /**
1587     * Get if the {@link PlatformReadyEvent} has been sent by WorldEdit. There is no way to query this within WorldEdit itself.
1588     */
1589    public boolean isWeInitialised() {
1590        return weInitialised;
1591    }
1592
1593    /**
1594     * Different ways of sorting {@link Plot plots}
1595     */
1596    public enum SortType {
1597        /**
1598         * Sort plots by their creation, using their index in the database
1599         */
1600        CREATION_DATE,
1601        /**
1602         * Sort plots by their creation timestamp
1603         */
1604        CREATION_DATE_TIMESTAMP,
1605        /**
1606         * Sort plots by when they were last modified
1607         */
1608        LAST_MODIFIED,
1609        /**
1610         * Sort plots based on their distance from the origin of the world
1611         */
1612        DISTANCE_FROM_ORIGIN
1613    }
1614
1615    private final class WEPlatformReadyListener {
1616
1617        @SuppressWarnings("unused")
1618        @Subscribe(priority = EventHandler.Priority.VERY_EARLY)
1619        public void onPlatformReady(PlatformReadyEvent event) {
1620            weInitialised = true;
1621            WorldEdit.getInstance().getEventBus().unregister(WEPlatformReadyListener.this);
1622        }
1623
1624    }
1625
1626}