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.MalformedURLException;
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                        new URL(logurl.toURI().toString().split("\\!")[0].replaceAll("jar:file", "file"))
214                                .toURI().getPath());
215            } catch (MalformedURLException | URISyntaxException | SecurityException e) {
216                e.printStackTrace();
217                this.jarFile = new File(this.platform.getDirectory().getParentFile(), "PlotSquared.jar");
218                if (!this.jarFile.exists()) {
219                    this.jarFile = new File(
220                            this.platform.getDirectory().getParentFile(),
221                            "PlotSquared-" + platform + ".jar"
222                    );
223                }
224            }
225
226            this.worldedit = WorldEdit.getInstance();
227            WorldEdit.getInstance().getEventBus().register(new WEPlatformReadyListener());
228
229            // Create Event utility class
230            this.eventDispatcher = new EventDispatcher(this.worldedit);
231            // Create plot listener
232            this.plotListener = new PlotListener(this.eventDispatcher);
233
234            // Copy files
235            copyFile("town.template", Settings.Paths.TEMPLATES);
236            copyFile("bridge.template", Settings.Paths.TEMPLATES);
237            copyFile("skyblock.template", Settings.Paths.TEMPLATES);
238            showDebug();
239        } catch (Throwable e) {
240            e.printStackTrace();
241        }
242    }
243
244    /**
245     * Gets an instance of PlotSquared.
246     *
247     * @return instance of PlotSquared
248     */
249    public static @NonNull PlotSquared get() {
250        return PlotSquared.instance;
251    }
252
253    /**
254     * Get the platform specific implementation of PlotSquared
255     *
256     * @return Platform implementation
257     */
258    public static @NonNull PlotPlatform<?> platform() {
259        if (instance != null && instance.platform != null) {
260            return instance.platform;
261        }
262        throw new IllegalStateException("Plot platform implementation is missing");
263    }
264
265    public void loadCaptionMap() throws Exception {
266        this.platform.copyCaptionMaps();
267        // Setup localization
268        CaptionMap captionMap;
269        if (Settings.Enabled_Components.PER_USER_LOCALE) {
270            captionMap = this.captionLoader.loadAll(this.platform.getDirectory().toPath().resolve("lang"));
271        } else {
272            String fileName = "messages_" + Settings.Enabled_Components.DEFAULT_LOCALE + ".json";
273            captionMap = this.captionLoader.loadOrCreateSingle(this.platform
274                    .getDirectory()
275                    .toPath()
276                    .resolve("lang")
277                    .resolve(fileName));
278        }
279        this.captionMaps.put(TranslatableCaption.DEFAULT_NAMESPACE, captionMap);
280        LOGGER.info(
281                "Loaded caption map for namespace 'plotsquared': {}",
282                this.captionMaps.get(TranslatableCaption.DEFAULT_NAMESPACE).getClass().getCanonicalName()
283        );
284    }
285
286    /**
287     * Get the platform specific {@link PlotAreaManager} instance
288     *
289     * @return Plot area manager
290     */
291    public @NonNull PlotAreaManager getPlotAreaManager() {
292        return this.platform.plotAreaManager();
293    }
294
295    public void startExpiryTasks() {
296        if (Settings.Enabled_Components.PLOT_EXPIRY) {
297            ExpireManager expireManager = PlotSquared.platform().expireManager();
298            expireManager.runAutomatedTask();
299            for (Settings.Auto_Clear settings : Settings.AUTO_CLEAR.getInstances()) {
300                ExpiryTask task = new ExpiryTask(settings, this.getPlotAreaManager());
301                expireManager.addTask(task);
302            }
303        }
304    }
305
306    public boolean isMainThread(final @NonNull Thread thread) {
307        return this.thread == thread;
308    }
309
310    /**
311     * Check if `version` is &gt;= `version2`.
312     *
313     * @param version  First version
314     * @param version2 Second version
315     * @return {@code true} if `version` is &gt;= `version2`
316     */
317    public boolean checkVersion(
318            final int[] version,
319            final int... version2
320    ) {
321        return version[0] > version2[0] || version[0] == version2[0] && version[1] > version2[1]
322                || version[0] == version2[0] && version[1] == version2[1] && version[2] >= version2[2];
323    }
324
325    /**
326     * Gets the current PlotSquared version.
327     *
328     * @return current version in config or null
329     */
330    public @NonNull PlotVersion getVersion() {
331        return this.version;
332    }
333
334    /**
335     * Gets the server platform this plugin is running on this is running on.
336     *
337     * <p>This will be either <b>Bukkit</b> or <b>Sponge</b></p>
338     *
339     * @return the server implementation
340     */
341    public @NonNull String getPlatform() {
342        return Settings.PLATFORM;
343    }
344
345    /**
346     * Add a global reference to a plot world.
347     * <p>
348     * You can remove the reference by calling {@link #removePlotArea(PlotArea)}
349     * </p>
350     *
351     * @param plotArea the {@link PlotArea} to add.
352     */
353    @SuppressWarnings("unchecked")
354    public void addPlotArea(final @NonNull PlotArea plotArea) {
355        HashMap<PlotId, Plot> plots;
356        if (plots_tmp == null || (plots = plots_tmp.remove(plotArea.toString())) == null) {
357            if (plotArea.getType() == PlotAreaType.PARTIAL) {
358                plots = this.plots_tmp != null ? this.plots_tmp.get(plotArea.getWorldName()) : null;
359                if (plots != null) {
360                    Iterator<Entry<PlotId, Plot>> iterator = plots.entrySet().iterator();
361                    while (iterator.hasNext()) {
362                        Entry<PlotId, Plot> next = iterator.next();
363                        PlotId id = next.getKey();
364                        if (plotArea.contains(id)) {
365                            next.getValue().setArea(plotArea);
366                            iterator.remove();
367                        }
368                    }
369                }
370            }
371        } else {
372            for (Plot entry : plots.values()) {
373                entry.setArea(plotArea);
374            }
375        }
376        Set<PlotCluster> clusters;
377        if (clustersTmp == null || (clusters = clustersTmp.remove(plotArea.toString())) == null) {
378            if (plotArea.getType() == PlotAreaType.PARTIAL) {
379                clusters = this.clustersTmp != null ?
380                        this.clustersTmp.get(plotArea.getWorldName()) :
381                        null;
382                if (clusters != null) {
383                    Iterator<PlotCluster> iterator = clusters.iterator();
384                    while (iterator.hasNext()) {
385                        PlotCluster next = iterator.next();
386                        if (next.intersects(plotArea.getMin(), plotArea.getMax())) {
387                            next.setArea(plotArea);
388                            iterator.remove();
389                        }
390                    }
391                }
392            }
393        } else {
394            for (PlotCluster cluster : clusters) {
395                cluster.setArea(plotArea);
396            }
397        }
398        getPlotAreaManager().addPlotArea(plotArea);
399        plotArea.setupBorder();
400        if (!Settings.Enabled_Components.PERSISTENT_ROAD_REGEN) {
401            return;
402        }
403        File file = new File(
404                this.platform.getDirectory() + File.separator + "persistent_regen_data_" + plotArea.getId()
405                        + "_" + plotArea.getWorldName());
406        if (!file.exists()) {
407            return;
408        }
409        TaskManager.runTaskAsync(() -> {
410            try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file))) {
411                List<Object> list = (List<Object>) ois.readObject();
412                ArrayList<int[]> regionInts = (ArrayList<int[]>) list.get(0);
413                ArrayList<int[]> chunkInts = (ArrayList<int[]>) list.get(1);
414                HashSet<BlockVector2> regions = new HashSet<>();
415                Set<BlockVector2> chunks = new HashSet<>();
416                regionInts.forEach(l -> regions.add(BlockVector2.at(l[0], l[1])));
417                chunkInts.forEach(l -> chunks.add(BlockVector2.at(l[0], l[1])));
418                int height = (int) list.get(2);
419                LOGGER.info(
420                        "Incomplete road regeneration found. Restarting in world {} with height {}",
421                        plotArea.getWorldName(),
422                        height
423                );
424                LOGGER.info("- Regions: {}", regions.size());
425                LOGGER.info("- Chunks: {}", chunks.size());
426                HybridUtils.UPDATE = true;
427                PlotSquared.platform().hybridUtils().scheduleRoadUpdate(plotArea, regions, height, chunks);
428            } catch (IOException | ClassNotFoundException e) {
429                LOGGER.error("Error restarting road regeneration", e);
430            } finally {
431                if (!file.delete()) {
432                    LOGGER.error("Error deleting persistent_regen_data_{}. Please delete this file manually", plotArea.getId());
433                }
434            }
435        });
436    }
437
438    /**
439     * Remove a plot world reference.
440     *
441     * @param area the {@link PlotArea} to remove
442     */
443    public void removePlotArea(final @NonNull PlotArea area) {
444        getPlotAreaManager().removePlotArea(area);
445        setPlotsTmp(area);
446    }
447
448    public void removePlotAreas(final @NonNull String world) {
449        for (final PlotArea area : this.getPlotAreaManager().getPlotAreasSet(world)) {
450            if (area.getWorldName().equals(world)) {
451                removePlotArea(area);
452            }
453        }
454    }
455
456    private void setPlotsTmp(final @NonNull PlotArea area) {
457        if (this.plots_tmp == null) {
458            this.plots_tmp = new HashMap<>();
459        }
460        HashMap<PlotId, Plot> map =
461                this.plots_tmp.computeIfAbsent(area.toString(), k -> new HashMap<>());
462        for (Plot plot : area.getPlots()) {
463            map.put(plot.getId(), plot);
464        }
465        if (this.clustersTmp == null) {
466            this.clustersTmp = new HashMap<>();
467        }
468        this.clustersTmp.put(area.toString(), area.getClusters());
469    }
470
471    public Set<PlotCluster> getClusters(final @NonNull String world) {
472        final Set<PlotCluster> set = new HashSet<>();
473        for (final PlotArea area : this.getPlotAreaManager().getPlotAreasSet(world)) {
474            set.addAll(area.getClusters());
475        }
476        return Collections.unmodifiableSet(set);
477
478    }
479
480    public List<Plot> sortPlotsByTemp(Collection<Plot> plots) {
481        int max = 0;
482        int overflowCount = 0;
483        for (Plot plot : plots) {
484            if (plot.temp > 0) {
485                if (plot.temp > max) {
486                    max = plot.temp;
487                }
488            } else {
489                overflowCount++;
490            }
491        }
492        Plot[] array = new Plot[max + 1];
493        List<Plot> overflow = new ArrayList<>(overflowCount);
494        for (Plot plot : plots) {
495            if (plot.temp <= 0) {
496                overflow.add(plot);
497            } else {
498                array[plot.temp] = plot;
499            }
500        }
501        ArrayList<Plot> result = new ArrayList<>(plots.size());
502        for (Plot plot : array) {
503            if (plot != null) {
504                result.add(plot);
505            }
506        }
507        overflow.sort(Comparator.comparingInt(Plot::hashCode));
508        result.addAll(overflow);
509        return result;
510    }
511
512    /**
513     * Sort plots by hashcode.
514     *
515     * @param plots the collection of plots to sort
516     * @return the sorted collection
517     */
518    private ArrayList<Plot> sortPlotsByHash(Collection<Plot> plots) {
519        int hardmax = 256000;
520        int max = 0;
521        int overflowSize = 0;
522        for (Plot plot : plots) {
523            int hash = MathMan.getPositiveId(plot.hashCode());
524            if (hash > max) {
525                if (hash >= hardmax) {
526                    overflowSize++;
527                } else {
528                    max = hash;
529                }
530            }
531        }
532        hardmax = Math.min(hardmax, max);
533        Plot[] cache = new Plot[hardmax + 1];
534        List<Plot> overflow = new ArrayList<>(overflowSize);
535        ArrayList<Plot> extra = new ArrayList<>();
536        for (Plot plot : plots) {
537            int hash = MathMan.getPositiveId(plot.hashCode());
538            if (hash < hardmax) {
539                if (hash >= 0) {
540                    cache[hash] = plot;
541                } else {
542                    extra.add(plot);
543                }
544            } else if (Math.abs(plot.getId().getX()) > 15446 || Math.abs(plot.getId().getY()) > 15446) {
545                extra.add(plot);
546            } else {
547                overflow.add(plot);
548            }
549        }
550        Plot[] overflowArray = overflow.toArray(new Plot[0]);
551        sortPlotsByHash(overflowArray);
552        ArrayList<Plot> result = new ArrayList<>(cache.length + overflowArray.length);
553        for (Plot plot : cache) {
554            if (plot != null) {
555                result.add(plot);
556            }
557        }
558        Collections.addAll(result, overflowArray);
559        result.addAll(extra);
560        return result;
561    }
562
563    /**
564     * Unchecked, use {@link #sortPlots(Collection, SortType, PlotArea)} instead which will in turn call this.
565     *
566     * @param input an array of plots to sort
567     */
568    @SuppressWarnings("unchecked")
569    private void sortPlotsByHash(final @NonNull Plot @NonNull [] input) {
570        List<Plot>[] bucket = new ArrayList[32];
571        Arrays.fill(bucket, new ArrayList<>());
572        boolean maxLength = false;
573        int placement = 1;
574        while (!maxLength) {
575            maxLength = true;
576            for (Plot plot : input) {
577                int tmp = MathMan.getPositiveId(plot.hashCode()) / placement;
578                bucket[tmp & 31].add(plot);
579                if (maxLength && tmp > 0) {
580                    maxLength = false;
581                }
582            }
583            int a = 0;
584            for (int i = 0; i < 32; i++) {
585                for (Plot plot : bucket[i]) {
586                    input[a++] = plot;
587                }
588                bucket[i].clear();
589            }
590            placement *= 32;
591        }
592    }
593
594    private @NonNull List<Plot> sortPlotsByTimestamp(final @NonNull Collection<Plot> plots) {
595        int hardMax = 256000;
596        int max = 0;
597        int overflowSize = 0;
598        for (final Plot plot : plots) {
599            int hash = MathMan.getPositiveId(plot.hashCode());
600            if (hash > max) {
601                if (hash >= hardMax) {
602                    overflowSize++;
603                } else {
604                    max = hash;
605                }
606            }
607        }
608        hardMax = Math.min(hardMax, max);
609        Plot[] cache = new Plot[hardMax + 1];
610        List<Plot> overflow = new ArrayList<>(overflowSize);
611        ArrayList<Plot> extra = new ArrayList<>();
612        for (Plot plot : plots) {
613            int hash = MathMan.getPositiveId(plot.hashCode());
614            if (hash < hardMax) {
615                if (hash >= 0) {
616                    cache[hash] = plot;
617                } else {
618                    extra.add(plot);
619                }
620            } else if (Math.abs(plot.getId().getX()) > 15446 || Math.abs(plot.getId().getY()) > 15446) {
621                extra.add(plot);
622            } else {
623                overflow.add(plot);
624            }
625        }
626        Plot[] overflowArray = overflow.toArray(new Plot[0]);
627        sortPlotsByHash(overflowArray);
628        ArrayList<Plot> result = new ArrayList<>(cache.length + overflowArray.length);
629        for (Plot plot : cache) {
630            if (plot != null) {
631                result.add(plot);
632            }
633        }
634        Collections.addAll(result, overflowArray);
635        result.addAll(extra);
636        return result;
637    }
638
639    /**
640     * Sort plots by creation timestamp.
641     *
642     * @param input Plots to sort
643     * @return Sorted list
644     */
645    private @NonNull List<Plot> sortPlotsByModified(final @NonNull Collection<Plot> input) {
646        List<Plot> list;
647        if (input instanceof List) {
648            list = (List<Plot>) input;
649        } else {
650            list = new ArrayList<>(input);
651        }
652        ExpireManager expireManager = PlotSquared.platform().expireManager();
653        list.sort(Comparator.comparingLong(a -> expireManager.getTimestamp(a.getOwnerAbs())));
654        return list;
655    }
656
657    /**
658     * Sort a collection of plots by world (with a priority world), then
659     * by hashcode.
660     *
661     * @param plots        the plots to sort
662     * @param type         The sorting method to use for each world (timestamp, or hash)
663     * @param priorityArea Use null, "world", or "gibberish" if you
664     *                     want default world order
665     * @return ArrayList of plot
666     */
667    public @NonNull List<Plot> sortPlots(
668            final @NonNull Collection<Plot> plots,
669            final @NonNull SortType type,
670            final @Nullable PlotArea priorityArea
671    ) {
672        // group by world
673        // sort each
674        HashMap<PlotArea, Collection<Plot>> map = new HashMap<>();
675        int totalSize = Arrays.stream(this.getPlotAreaManager().getAllPlotAreas()).mapToInt(PlotArea::getPlotCount).sum();
676        if (plots.size() == totalSize) {
677            for (PlotArea area : getPlotAreaManager().getAllPlotAreas()) {
678                map.put(area, area.getPlots());
679            }
680        } else {
681            for (PlotArea area : getPlotAreaManager().getAllPlotAreas()) {
682                map.put(area, new ArrayList<>(0));
683            }
684            Collection<Plot> lastList = null;
685            PlotArea lastWorld = null;
686            for (Plot plot : plots) {
687                if (lastWorld == plot.getArea()) {
688                    lastList.add(plot);
689                } else {
690                    lastWorld = plot.getArea();
691                    lastList = map.get(lastWorld);
692                    lastList.add(plot);
693                }
694            }
695        }
696        List<PlotArea> areas = Arrays.asList(getPlotAreaManager().getAllPlotAreas());
697        areas.sort((a, b) -> {
698            if (priorityArea != null) {
699                if (a.equals(priorityArea)) {
700                    return -1;
701                } else if (b.equals(priorityArea)) {
702                    return 1;
703                }
704            }
705            return a.hashCode() - b.hashCode();
706        });
707        ArrayList<Plot> toReturn = new ArrayList<>(plots.size());
708        for (PlotArea area : areas) {
709            switch (type) {
710                case CREATION_DATE -> toReturn.addAll(sortPlotsByTemp(map.get(area)));
711                case CREATION_DATE_TIMESTAMP -> toReturn.addAll(sortPlotsByTimestamp(map.get(area)));
712                case DISTANCE_FROM_ORIGIN -> toReturn.addAll(sortPlotsByHash(map.get(area)));
713                case LAST_MODIFIED -> toReturn.addAll(sortPlotsByModified(map.get(area)));
714                default -> {
715                }
716            }
717        }
718        return toReturn;
719    }
720
721    public void setPlots(final @NonNull Map<String, HashMap<PlotId, Plot>> plots) {
722        if (this.plots_tmp == null) {
723            this.plots_tmp = new HashMap<>();
724        }
725        for (final Entry<String, HashMap<PlotId, Plot>> entry : plots.entrySet()) {
726            final String world = entry.getKey();
727            final PlotArea plotArea = this.getPlotAreaManager().getPlotArea(world, null);
728            if (plotArea == null) {
729                Map<PlotId, Plot> map = this.plots_tmp.computeIfAbsent(world, k -> new HashMap<>());
730                map.putAll(entry.getValue());
731            } else {
732                for (Plot plot : entry.getValue().values()) {
733                    plot.setArea(plotArea);
734                    plotArea.addPlot(plot);
735                }
736            }
737        }
738    }
739
740    /**
741     * Unregisters a plot from local memory without calling the database.
742     *
743     * @param plot      the plot to remove
744     * @param callEvent If to call an event about the plot being removed
745     * @return {@code true} if plot existed | {@code false} if it didn't
746     */
747    public boolean removePlot(
748            final @NonNull Plot plot,
749            final boolean callEvent
750    ) {
751        if (plot == null) {
752            return false;
753        }
754        if (callEvent) {
755            eventDispatcher.callDelete(plot);
756        }
757        if (plot.getArea().removePlot(plot.getId())) {
758            PlotId last = (PlotId) plot.getArea().getMeta("lastPlot");
759            int last_max = Math.max(Math.abs(last.getX()), Math.abs(last.getY()));
760            int this_max = Math.max(Math.abs(plot.getId().getX()), Math.abs(plot.getId().getY()));
761            if (this_max < last_max) {
762                plot.getArea().setMeta("lastPlot", plot.getId());
763            }
764            if (callEvent) {
765                eventDispatcher.callPostDelete(plot);
766            }
767            return true;
768        }
769        return false;
770    }
771
772    /**
773     * This method is called by the PlotGenerator class normally.
774     * <ul>
775     * <li>Initializes the PlotArea and PlotManager classes
776     * <li>Registers the PlotArea and PlotManager classes
777     * <li>Loads (and/or generates) the PlotArea configuration
778     * <li>Sets up the world border if configured
779     * </ul>
780     *
781     * <p>If loading an augmented plot world:
782     * <ul>
783     * <li>Creates the AugmentedPopulator classes
784     * <li>Injects the AugmentedPopulator classes if required
785     * </ul>
786     *
787     * @param world         the world to load
788     * @param baseGenerator The generator for that world, or null
789     */
790    public void loadWorld(
791            final @NonNull String world,
792            final @Nullable GeneratorWrapper<?> baseGenerator
793    ) {
794        if (world.equals("CheckingPlotSquaredGenerator")) {
795            return;
796        }
797        if (!this.getPlotAreaManager().addWorld(world)) {
798            return;
799        }
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     * Setup 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}