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        if (!this.getPlotAreaManager().addWorld(world)) {
799            return;
800        }
801        Set<String> worlds;
802        if (this.worldConfiguration.contains("worlds")) {
803            worlds = this.worldConfiguration.getConfigurationSection("worlds").getKeys(false);
804        } else {
805            worlds = new HashSet<>();
806        }
807        String path = "worlds." + world;
808        ConfigurationSection worldSection = this.worldConfiguration.getConfigurationSection(path);
809        PlotAreaType type;
810        if (worldSection != null) {
811            type = ConfigurationUtil.getType(worldSection);
812        } else {
813            type = PlotAreaType.NORMAL;
814        }
815        if (type == PlotAreaType.NORMAL) {
816            if (getPlotAreaManager().getPlotAreas(world, null).length != 0) {
817                return;
818            }
819            IndependentPlotGenerator plotGenerator;
820            if (baseGenerator != null && baseGenerator.isFull()) {
821                plotGenerator = baseGenerator.getPlotGenerator();
822            } else if (worldSection != null) {
823                String secondaryGeneratorName = worldSection.getString("generator.plugin");
824                GeneratorWrapper<?> secondaryGenerator =
825                        this.platform.getGenerator(world, secondaryGeneratorName);
826                if (secondaryGenerator != null && secondaryGenerator.isFull()) {
827                    plotGenerator = secondaryGenerator.getPlotGenerator();
828                } else {
829                    String primaryGeneratorName = worldSection.getString("generator.init");
830                    GeneratorWrapper<?> primaryGenerator =
831                            this.platform.getGenerator(world, primaryGeneratorName);
832                    if (primaryGenerator != null && primaryGenerator.isFull()) {
833                        plotGenerator = primaryGenerator.getPlotGenerator();
834                    } else {
835                        return;
836                    }
837                }
838            } else {
839                return;
840            }
841            // Conventional plot generator
842            PlotArea plotArea = plotGenerator.getNewPlotArea(world, null, null, null);
843            PlotManager plotManager = plotArea.getPlotManager();
844            LOGGER.info("Detected world load for '{}'", world);
845            LOGGER.info("- generator: {}>{}", baseGenerator, plotGenerator);
846            LOGGER.info("- plot world: {}", plotArea.getClass().getCanonicalName());
847            LOGGER.info("- plot area manager: {}", plotManager.getClass().getCanonicalName());
848            if (!this.worldConfiguration.contains(path)) {
849                this.worldConfiguration.createSection(path);
850                worldSection = this.worldConfiguration.getConfigurationSection(path);
851            }
852            plotArea.saveConfiguration(worldSection);
853            plotArea.loadDefaultConfiguration(worldSection);
854            try {
855                this.worldConfiguration.save(this.worldsFile);
856            } catch (IOException e) {
857                e.printStackTrace();
858            }
859            // Now add it
860            addPlotArea(plotArea);
861            plotGenerator.initialize(plotArea);
862        } else {
863            if (!worlds.contains(world)) {
864                return;
865            }
866            ConfigurationSection areasSection = worldSection.getConfigurationSection("areas");
867            if (areasSection == null) {
868                if (getPlotAreaManager().getPlotAreas(world, null).length != 0) {
869                    return;
870                }
871                LOGGER.info("Detected world load for '{}'", world);
872                String gen_string = worldSection.getString("generator.plugin", platform.pluginName());
873                if (type == PlotAreaType.PARTIAL) {
874                    Set<PlotCluster> clusters =
875                            this.clustersTmp != null ? this.clustersTmp.get(world) : new HashSet<>();
876                    if (clusters == null) {
877                        throw new IllegalArgumentException("No cluster exists for world: " + world);
878                    }
879                    ArrayDeque<PlotArea> toLoad = new ArrayDeque<>();
880                    for (PlotCluster cluster : clusters) {
881                        PlotId pos1 = cluster.getP1(); // Cluster pos1
882                        PlotId pos2 = cluster.getP2(); // Cluster pos2
883                        String name = cluster.getName(); // Cluster name
884                        String fullId = name + "-" + pos1 + "-" + pos2;
885                        worldSection.createSection("areas." + fullId);
886                        DBFunc.replaceWorld(world, world + ";" + name, pos1, pos2); // NPE
887                        LOGGER.info("- {}-{}-{}", name, pos1, pos2);
888                        GeneratorWrapper<?> areaGen = this.platform.getGenerator(world, gen_string);
889                        if (areaGen == null) {
890                            throw new IllegalArgumentException("Invalid Generator: " + gen_string);
891                        }
892                        PlotArea pa =
893                                areaGen.getPlotGenerator().getNewPlotArea(world, name, pos1, pos2);
894                        pa.saveConfiguration(worldSection);
895                        pa.loadDefaultConfiguration(worldSection);
896                        try {
897                            this.worldConfiguration.save(this.worldsFile);
898                        } catch (IOException e) {
899                            e.printStackTrace();
900                        }
901                        LOGGER.info("| generator: {}>{}", baseGenerator, areaGen);
902                        LOGGER.info("| plot world: {}", pa.getClass().getCanonicalName());
903                        LOGGER.info("| manager: {}", pa.getPlotManager().getClass().getCanonicalName());
904                        LOGGER.info("Note: Area created for cluster '{}' (invalid or old configuration?)", name);
905                        areaGen.getPlotGenerator().initialize(pa);
906                        areaGen.augment(pa);
907                        toLoad.add(pa);
908                    }
909                    for (PlotArea area : toLoad) {
910                        addPlotArea(area);
911                    }
912                    return;
913                }
914                GeneratorWrapper<?> areaGen = this.platform.getGenerator(world, gen_string);
915                if (areaGen == null) {
916                    throw new IllegalArgumentException("Invalid Generator: " + gen_string);
917                }
918                PlotArea pa = areaGen.getPlotGenerator().getNewPlotArea(world, null, null, null);
919                LOGGER.info("- generator: {}>{}", baseGenerator, areaGen);
920                LOGGER.info("- plot world: {}", pa.getClass().getCanonicalName());
921                LOGGER.info("- plot area manager: {}", pa.getPlotManager().getClass().getCanonicalName());
922                if (!this.worldConfiguration.contains(path)) {
923                    this.worldConfiguration.createSection(path);
924                    worldSection = this.worldConfiguration.getConfigurationSection(path);
925                }
926                pa.saveConfiguration(worldSection);
927                pa.loadDefaultConfiguration(worldSection);
928                try {
929                    this.worldConfiguration.save(this.worldsFile);
930                } catch (IOException e) {
931                    e.printStackTrace();
932                }
933                areaGen.getPlotGenerator().initialize(pa);
934                areaGen.augment(pa);
935                addPlotArea(pa);
936                return;
937            }
938            if (type == PlotAreaType.AUGMENTED) {
939                throw new IllegalArgumentException(
940                        "Invalid type for multi-area world. Expected `PARTIAL`, got `"
941                                + PlotAreaType.AUGMENTED + "`");
942            }
943            for (String areaId : areasSection.getKeys(false)) {
944                LOGGER.info("- {}", areaId);
945                String[] split = areaId.split("(?<=[^;-])-");
946                if (split.length != 3) {
947                    throw new IllegalArgumentException("Invalid Area identifier: " + areaId
948                            + ". Expected form `<name>-<pos1>-<pos2>`");
949                }
950                String name = split[0];
951                PlotId pos1 = PlotId.fromString(split[1]);
952                PlotId pos2 = PlotId.fromString(split[2]);
953                if (name.isEmpty()) {
954                    throw new IllegalArgumentException("Invalid Area identifier: " + areaId
955                            + ". Expected form `<name>-<x1;z1>-<x2;z2>`");
956                }
957                final PlotArea existing = this.getPlotAreaManager().getPlotArea(world, name);
958                if (existing != null && name.equals(existing.getId())) {
959                    continue;
960                }
961                ConfigurationSection section = areasSection.getConfigurationSection(areaId);
962                YamlConfiguration clone = new YamlConfiguration();
963                for (String key : section.getKeys(true)) {
964                    if (section.get(key) instanceof MemorySection) {
965                        continue;
966                    }
967                    if (!clone.contains(key)) {
968                        clone.set(key, section.get(key));
969                    }
970                }
971                for (String key : worldSection.getKeys(true)) {
972                    if (worldSection.get(key) instanceof MemorySection) {
973                        continue;
974                    }
975                    if (!key.startsWith("areas") && !clone.contains(key)) {
976                        clone.set(key, worldSection.get(key));
977                    }
978                }
979                String gen_string = clone.getString("generator.plugin", platform.pluginName());
980                GeneratorWrapper<?> areaGen = this.platform.getGenerator(world, gen_string);
981                if (areaGen == null) {
982                    throw new IllegalArgumentException("Invalid Generator: " + gen_string);
983                }
984                PlotArea pa = areaGen.getPlotGenerator().getNewPlotArea(world, name, pos1, pos2);
985                pa.saveConfiguration(clone);
986                // netSections is the combination of
987                for (String key : clone.getKeys(true)) {
988                    if (clone.get(key) instanceof MemorySection) {
989                        continue;
990                    }
991                    if (!worldSection.contains(key)) {
992                        worldSection.set(key, clone.get(key));
993                    } else {
994                        Object value = worldSection.get(key);
995                        if (!Objects.equals(value, clone.get(key))) {
996                            section.set(key, clone.get(key));
997                        }
998                    }
999                }
1000                pa.loadDefaultConfiguration(clone);
1001                try {
1002                    this.worldConfiguration.save(this.worldsFile);
1003                } catch (IOException e) {
1004                    e.printStackTrace();
1005                }
1006                LOGGER.info("Detected area load for '{}'", world);
1007                LOGGER.info("| generator: {}>{}", baseGenerator, areaGen);
1008                LOGGER.info("| plot world: {}", pa);
1009                LOGGER.info("| manager: {}", pa.getPlotManager());
1010                areaGen.getPlotGenerator().initialize(pa);
1011                areaGen.augment(pa);
1012                addPlotArea(pa);
1013            }
1014        }
1015    }
1016
1017    /**
1018     * Setup the configuration for a plot world based on world arguments.
1019     * <p>
1020     *
1021     * <i>e.g. /mv create &lt;world&gt; normal -g PlotSquared:&lt;args&gt;</i>
1022     *
1023     * @param world     The name of the world
1024     * @param args      The arguments
1025     * @param generator the plot generator
1026     * @return boolean | if valid arguments were provided
1027     */
1028    public boolean setupPlotWorld(
1029            final @NonNull String world,
1030            final @Nullable String args,
1031            final @NonNull IndependentPlotGenerator generator
1032    ) {
1033        if (args != null && !args.isEmpty()) {
1034            // save configuration
1035
1036            final List<String> validArguments = Arrays
1037                    .asList("s=", "size=", "g=", "gap=", "h=", "height=", "minh=", "minheight=", "maxh=", "maxheight=",
1038                            "f=", "floor=", "m=", "main=", "w=", "wall=", "b=", "border="
1039                    );
1040
1041            // Calculate the number of expected arguments
1042            int expected = (int) validArguments.stream()
1043                    .filter(validArgument -> args.toLowerCase(Locale.ENGLISH).contains(validArgument))
1044                    .count();
1045
1046            String[] split = args.toLowerCase(Locale.ENGLISH).split(",(?![^\\(\\[]*[\\]\\)])");
1047
1048            if (split.length > expected) {
1049                // This means we have multi-block block buckets
1050                String[] combinedArgs = new String[expected];
1051                int index = 0;
1052
1053                StringBuilder argBuilder = new StringBuilder();
1054                outer:
1055                for (final String string : split) {
1056                    for (final String validArgument : validArguments) {
1057                        if (string.contains(validArgument)) {
1058                            if (!argBuilder.toString().isEmpty()) {
1059                                combinedArgs[index++] = argBuilder.toString();
1060                                argBuilder = new StringBuilder();
1061                            }
1062                            argBuilder.append(string);
1063                            continue outer;
1064                        }
1065                    }
1066                    if (argBuilder.toString().charAt(argBuilder.length() - 1) != '=') {
1067                        argBuilder.append(",");
1068                    }
1069                    argBuilder.append(string);
1070                }
1071
1072                if (!argBuilder.toString().isEmpty()) {
1073                    combinedArgs[index] = argBuilder.toString();
1074                }
1075
1076                split = combinedArgs;
1077            }
1078
1079            final HybridPlotWorldFactory hybridPlotWorldFactory = this.platform
1080                    .injector()
1081                    .getInstance(HybridPlotWorldFactory.class);
1082            final HybridPlotWorld plotWorld = hybridPlotWorldFactory.create(world, null, generator, null, null);
1083
1084            for (String element : split) {
1085                String[] pair = element.split("=");
1086                if (pair.length != 2) {
1087                    LOGGER.error("No value provided for '{}'", element);
1088                    return false;
1089                }
1090                String key = pair[0].toLowerCase();
1091                String value = pair[1];
1092                try {
1093                    String base = "worlds." + world + ".";
1094                    switch (key) {
1095                        case "s", "size" -> this.worldConfiguration.set(
1096                                base + "plot.size",
1097                                ConfigurationUtil.INTEGER.parseString(value).shortValue()
1098                        );
1099                        case "g", "gap" -> this.worldConfiguration.set(
1100                                base + "road.width",
1101                                ConfigurationUtil.INTEGER.parseString(value).shortValue()
1102                        );
1103                        case "h", "height" -> {
1104                            this.worldConfiguration.set(
1105                                    base + "road.height",
1106                                    ConfigurationUtil.INTEGER.parseString(value).shortValue()
1107                            );
1108                            this.worldConfiguration.set(
1109                                    base + "plot.height",
1110                                    ConfigurationUtil.INTEGER.parseString(value).shortValue()
1111                            );
1112                            this.worldConfiguration.set(
1113                                    base + "wall.height",
1114                                    ConfigurationUtil.INTEGER.parseString(value).shortValue()
1115                            );
1116                        }
1117                        case "minh", "minheight" -> this.worldConfiguration.set(
1118                                base + "world.min_gen_height",
1119                                ConfigurationUtil.INTEGER.parseString(value).shortValue()
1120                        );
1121                        case "maxh", "maxheight" -> this.worldConfiguration.set(
1122                                base + "world.max_gen_height",
1123                                ConfigurationUtil.INTEGER.parseString(value).shortValue()
1124                        );
1125                        case "f", "floor" -> this.worldConfiguration.set(
1126                                base + "plot.floor",
1127                                ConfigurationUtil.BLOCK_BUCKET.parseString(value).toString()
1128                        );
1129                        case "m", "main" -> this.worldConfiguration.set(
1130                                base + "plot.filling",
1131                                ConfigurationUtil.BLOCK_BUCKET.parseString(value).toString()
1132                        );
1133                        case "w", "wall" -> this.worldConfiguration.set(
1134                                base + "wall.filling",
1135                                ConfigurationUtil.BLOCK_BUCKET.parseString(value).toString()
1136                        );
1137                        case "b", "border" -> this.worldConfiguration.set(
1138                                base + "wall.block",
1139                                ConfigurationUtil.BLOCK_BUCKET.parseString(value).toString()
1140                        );
1141                        default -> {
1142                            LOGGER.error("Key not found: {}", element);
1143                            return false;
1144                        }
1145                    }
1146                } catch (Exception e) {
1147                    LOGGER.error("Invalid value '{}' for arg '{}'", value, element);
1148                    e.printStackTrace();
1149                    return false;
1150                }
1151            }
1152            try {
1153                ConfigurationSection section =
1154                        this.worldConfiguration.getConfigurationSection("worlds." + world);
1155                plotWorld.saveConfiguration(section);
1156                plotWorld.loadDefaultConfiguration(section);
1157                this.worldConfiguration.save(this.worldsFile);
1158            } catch (IOException e) {
1159                e.printStackTrace();
1160            }
1161        }
1162        return true;
1163    }
1164
1165    /**
1166     * Copies a file from inside the jar to a location
1167     *
1168     * @param file   Name of the file inside PlotSquared.jar
1169     * @param folder The output location relative to /plugins/PlotSquared/
1170     */
1171    public void copyFile(
1172            final @NonNull String file,
1173            final @NonNull String folder
1174    ) {
1175        try {
1176            File output = this.platform.getDirectory();
1177            if (!output.exists()) {
1178                output.mkdirs();
1179            }
1180            File newFile = FileUtils.getFile(output, folder + File.separator + file);
1181            if (newFile.exists()) {
1182                return;
1183            }
1184            try (InputStream stream = this.platform.getClass().getResourceAsStream(file)) {
1185                byte[] buffer = new byte[2048];
1186                if (stream == null) {
1187                    try (ZipInputStream zis = new ZipInputStream(
1188                            new FileInputStream(this.jarFile))) {
1189                        ZipEntry ze = zis.getNextEntry();
1190                        while (ze != null) {
1191                            String name = ze.getName();
1192                            if (name.equals(file)) {
1193                                new File(newFile.getParent()).mkdirs();
1194                                try (FileOutputStream fos = new FileOutputStream(newFile)) {
1195                                    int len;
1196                                    while ((len = zis.read(buffer)) > 0) {
1197                                        fos.write(buffer, 0, len);
1198                                    }
1199                                }
1200                                ze = null;
1201                            } else {
1202                                ze = zis.getNextEntry();
1203                            }
1204                        }
1205                        zis.closeEntry();
1206                    }
1207                    return;
1208                }
1209                newFile.createNewFile();
1210                try (FileOutputStream fos = new FileOutputStream(newFile)) {
1211                    int len;
1212                    while ((len = stream.read(buffer)) > 0) {
1213                        fos.write(buffer, 0, len);
1214                    }
1215                }
1216            }
1217        } catch (IOException e) {
1218            LOGGER.error("Could not save {}", file);
1219            e.printStackTrace();
1220        }
1221    }
1222
1223    /**
1224     * Safely closes the database connection.
1225     */
1226    public void disable() {
1227        try {
1228            eventDispatcher.unregisterAll();
1229            checkRoadRegenPersistence();
1230            // Validate that all data in the db is correct
1231            final HashSet<Plot> plots = new HashSet<>();
1232            try {
1233                forEachPlotRaw(plots::add);
1234            } catch (final Exception ignored) {
1235            }
1236            DBFunc.validatePlots(plots);
1237
1238            // Close the connection
1239            DBFunc.close();
1240        } catch (NullPointerException throwable) {
1241            LOGGER.error("Could not close database connection", throwable);
1242            throwable.printStackTrace();
1243        }
1244    }
1245
1246    /**
1247     * Handle road regen persistence
1248     */
1249    private void checkRoadRegenPersistence() {
1250        if (!HybridUtils.UPDATE || !Settings.Enabled_Components.PERSISTENT_ROAD_REGEN || (
1251                HybridUtils.regions.isEmpty() && HybridUtils.chunks.isEmpty())) {
1252            return;
1253        }
1254        LOGGER.info("Road regeneration incomplete. Saving incomplete regions to disk");
1255        LOGGER.info("- regions: {}", HybridUtils.regions.size());
1256        LOGGER.info("- chunks: {}", HybridUtils.chunks.size());
1257        ArrayList<int[]> regions = new ArrayList<>();
1258        ArrayList<int[]> chunks = new ArrayList<>();
1259        for (BlockVector2 r : HybridUtils.regions) {
1260            regions.add(new int[]{r.getBlockX(), r.getBlockZ()});
1261        }
1262        for (BlockVector2 c : HybridUtils.chunks) {
1263            chunks.add(new int[]{c.getBlockX(), c.getBlockZ()});
1264        }
1265        List<Object> list = new ArrayList<>();
1266        list.add(regions);
1267        list.add(chunks);
1268        list.add(HybridUtils.height);
1269        File file = new File(
1270                this.platform.getDirectory() + File.separator + "persistent_regen_data_" + HybridUtils.area
1271                        .getId() + "_" + HybridUtils.area.getWorldName());
1272        if (file.exists() && !file.delete()) {
1273            LOGGER.error("persistent_regene_data file already exists and could not be deleted");
1274            return;
1275        }
1276        try (ObjectOutputStream oos = new ObjectOutputStream(
1277                Files.newOutputStream(file.toPath(), StandardOpenOption.CREATE_NEW))) {
1278            oos.writeObject(list);
1279        } catch (IOException e) {
1280            LOGGER.error("Error creating persistent_region_data file", e);
1281        }
1282    }
1283
1284    /**
1285     * Set up the database connection.
1286     */
1287    public void setupDatabase() {
1288        try {
1289            if (DBFunc.dbManager != null) {
1290                DBFunc.dbManager.close();
1291            }
1292            Database database;
1293            if (Storage.MySQL.USE) {
1294                database = new MySQL(Storage.MySQL.HOST, Storage.MySQL.PORT, Storage.MySQL.DATABASE,
1295                        Storage.MySQL.USER, Storage.MySQL.PASSWORD
1296                );
1297            } else if (Storage.SQLite.USE) {
1298                File file = FileUtils.getFile(platform.getDirectory(), Storage.SQLite.DB + ".db");
1299                database = new SQLite(file);
1300            } else {
1301                LOGGER.error("No storage type is set. Disabling PlotSquared");
1302                this.platform.shutdown(); //shutdown used instead of disable because no database is set
1303                return;
1304            }
1305            DBFunc.dbManager = new SQLManager(
1306                    database,
1307                    Storage.PREFIX,
1308                    this.eventDispatcher,
1309                    this.plotListener,
1310                    this.worldConfiguration
1311            );
1312            this.plots_tmp = DBFunc.getPlots();
1313            if (getPlotAreaManager() instanceof SinglePlotAreaManager) {
1314                SinglePlotArea area = ((SinglePlotAreaManager) getPlotAreaManager()).getArea();
1315                addPlotArea(area);
1316                ConfigurationSection section = worldConfiguration.getConfigurationSection("worlds.*");
1317                if (section == null) {
1318                    section = worldConfiguration.createSection("worlds.*");
1319                }
1320                area.saveConfiguration(section);
1321                area.loadDefaultConfiguration(section);
1322            }
1323            this.clustersTmp = DBFunc.getClusters();
1324            LOGGER.info("Connection to database established. Type: {}", Storage.MySQL.USE ? "MySQL" : "SQLite");
1325        } catch (ClassNotFoundException | SQLException e) {
1326            LOGGER.error(
1327                    "Failed to open database connection ({}). Disabling PlotSquared",
1328                    Storage.MySQL.USE ? "MySQL" : "SQLite"
1329            );
1330            LOGGER.error("==== Here is an ugly stacktrace, if you are interested in those things ===");
1331            e.printStackTrace();
1332            LOGGER.error("==== End of stacktrace ====");
1333            LOGGER.error(
1334                    "Please go to the {} 'storage.yml' and configure the database correctly",
1335                    platform.pluginName()
1336            );
1337            this.platform.shutdown(); //shutdown used instead of disable because of database error
1338        }
1339    }
1340
1341    /**
1342     * Setup the default configuration.
1343     */
1344    public void setupConfig() {
1345        String lastVersionString = this.getConfig().getString("version");
1346        if (lastVersionString != null) {
1347            String[] split = lastVersionString.split("\\.");
1348            int[] lastVersion = new int[]{Integer.parseInt(split[0]), Integer.parseInt(split[1]),
1349                    Integer.parseInt(split[2])};
1350            if (checkVersion(new int[]{3, 4, 0}, lastVersion)) {
1351                Settings.convertLegacy(configFile);
1352                if (getConfig().contains("worlds")) {
1353                    ConfigurationSection worldSection =
1354                            getConfig().getConfigurationSection("worlds");
1355                    worldConfiguration.set("worlds", worldSection);
1356                    try {
1357                        worldConfiguration.save(worldsFile);
1358                    } catch (IOException e) {
1359                        LOGGER.error("Failed to save worlds.yml", e);
1360                        e.printStackTrace();
1361                    }
1362                }
1363                Settings.save(configFile);
1364            }
1365        }
1366        Settings.load(configFile);
1367        //Sets the version information for the settings.yml file
1368        try (InputStream stream = getClass().getResourceAsStream("/plugin.properties")) {
1369            try (BufferedReader br = new BufferedReader(new InputStreamReader(stream))) {
1370                String versionString = br.readLine();
1371                String commitString = br.readLine();
1372                String dateString = br.readLine();
1373                this.version = PlotVersion.tryParse(versionString, commitString, dateString);
1374            }
1375        } catch (IOException throwable) {
1376            throwable.printStackTrace();
1377        }
1378        Settings.save(configFile);
1379        config = YamlConfiguration.loadConfiguration(configFile);
1380    }
1381
1382    /**
1383     * Setup all configuration files<br>
1384     * - Config: settings.yml<br>
1385     * - Storage: storage.yml<br>
1386     *
1387     * @return success or not
1388     */
1389    public boolean setupConfigs() {
1390        File folder = new File(this.platform.getDirectory(), "config");
1391        if (!folder.exists() && !folder.mkdirs()) {
1392            LOGGER.error("Failed to create the {} config folder. Please create it manually", this.platform.getDirectory());
1393        }
1394        try {
1395            this.worldsFile = new File(folder, "worlds.yml");
1396            if (!this.worldsFile.exists() && !this.worldsFile.createNewFile()) {
1397                LOGGER.error("Could not create the worlds file. Please create 'worlds.yml' manually");
1398            }
1399            this.worldConfiguration = YamlConfiguration.loadConfiguration(this.worldsFile);
1400
1401            if (this.worldConfiguration.contains("worlds")) {
1402                if (!this.worldConfiguration.contains("configuration_version") || (
1403                        !this.worldConfiguration.getString("configuration_version")
1404                                .equalsIgnoreCase(LegacyConverter.CONFIGURATION_VERSION) && !this.worldConfiguration
1405                                .getString("configuration_version").equalsIgnoreCase("v5"))) {
1406                    // Conversion needed
1407                    LOGGER.info("A legacy configuration file was detected. Conversion will be attempted.");
1408                    try {
1409                        com.google.common.io.Files
1410                                .copy(this.worldsFile, new File(folder, "worlds.yml.old"));
1411                        LOGGER.info("A copy of worlds.yml has been saved in the file worlds.yml.old");
1412                        final ConfigurationSection worlds =
1413                                this.worldConfiguration.getConfigurationSection("worlds");
1414                        final LegacyConverter converter = new LegacyConverter(worlds);
1415                        converter.convert();
1416                        this.worldConfiguration.set("worlds", worlds);
1417                        this.setConfigurationVersion(LegacyConverter.CONFIGURATION_VERSION);
1418                        LOGGER.info(
1419                                "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.");
1420                    } catch (final Exception e) {
1421                        LOGGER.error("Failed to convert the legacy configuration file. See stack trace for information.", e);
1422                    }
1423                    // Disable plugin
1424                    this.platform.shutdown();
1425                    return false;
1426                }
1427            } else {
1428                this.worldConfiguration.set("configuration_version", LegacyConverter.CONFIGURATION_VERSION);
1429            }
1430        } catch (IOException ignored) {
1431            LOGGER.error("Failed to save worlds.yml");
1432        }
1433        try {
1434            this.configFile = new File(folder, "settings.yml");
1435            if (!this.configFile.exists() && !this.configFile.createNewFile()) {
1436                LOGGER.error("Could not create the settings file. Please create 'settings.yml' manually");
1437            }
1438            this.config = YamlConfiguration.loadConfiguration(this.configFile);
1439            setupConfig();
1440        } catch (IOException ignored) {
1441            LOGGER.error("Failed to save settings.yml");
1442        }
1443        try {
1444            this.storageFile = new File(folder, "storage.yml");
1445            if (!this.storageFile.exists() && !this.storageFile.createNewFile()) {
1446                LOGGER.error("Could not create the storage settings file. Please create 'storage.yml' manually");
1447            }
1448            YamlConfiguration.loadConfiguration(this.storageFile);
1449            setupStorage();
1450        } catch (IOException ignored) {
1451            LOGGER.error("Failed to save storage.yml");
1452        }
1453        return true;
1454    }
1455
1456    public @NonNull String getConfigurationVersion() {
1457        return this.worldConfiguration.get("configuration_version", LegacyConverter.CONFIGURATION_VERSION)
1458                .toString();
1459    }
1460
1461    public void setConfigurationVersion(final @NonNull String newVersion) throws IOException {
1462        this.worldConfiguration.set("configuration_version", newVersion);
1463        this.worldConfiguration.save(this.worldsFile);
1464    }
1465
1466    /**
1467     * Setup the storage file (load + save missing nodes).
1468     */
1469    private void setupStorage() {
1470        Storage.load(storageFile);
1471        Storage.save(storageFile);
1472        YamlConfiguration.loadConfiguration(storageFile);
1473    }
1474
1475    /**
1476     * Show startup debug information.
1477     */
1478    private void showDebug() {
1479        if (Settings.DEBUG) {
1480            Map<String, Object> components = Settings.getFields(Settings.Enabled_Components.class);
1481            for (Entry<String, Object> component : components.entrySet()) {
1482                LOGGER.info("Key: {} | Value: {}", component.getKey(), component.getValue());
1483            }
1484        }
1485    }
1486
1487    public void forEachPlotRaw(final @NonNull Consumer<Plot> consumer) {
1488        for (final PlotArea area : this.getPlotAreaManager().getAllPlotAreas()) {
1489            area.getPlots().forEach(consumer);
1490        }
1491        if (this.plots_tmp != null) {
1492            for (final HashMap<PlotId, Plot> entry : this.plots_tmp.values()) {
1493                entry.values().forEach(consumer);
1494            }
1495        }
1496    }
1497
1498    /**
1499     * Check if the chunk uses vanilla/non-PlotSquared generation
1500     *
1501     * @param world            World name
1502     * @param chunkCoordinates Chunk coordinates
1503     * @return {@code true} if the chunk uses non-standard generation, {@code false} if not
1504     */
1505    public boolean isNonStandardGeneration(
1506            final @NonNull String world,
1507            final @NonNull BlockVector2 chunkCoordinates
1508    ) {
1509        final Location location = Location.at(world, chunkCoordinates.getBlockX() << 4, 64, chunkCoordinates.getBlockZ() << 4);
1510        final PlotArea area = getPlotAreaManager().getApplicablePlotArea(location);
1511        if (area == null) {
1512            return true;
1513        }
1514        return area.getTerrain() != PlotAreaTerrainType.NONE;
1515    }
1516
1517    public @NonNull YamlConfiguration getConfig() {
1518        return config;
1519    }
1520
1521    public @NonNull UUIDPipeline getImpromptuUUIDPipeline() {
1522        return this.impromptuUUIDPipeline;
1523    }
1524
1525    public @NonNull UUIDPipeline getBackgroundUUIDPipeline() {
1526        return this.backgroundUUIDPipeline;
1527    }
1528
1529    public @NonNull WorldEdit getWorldEdit() {
1530        return this.worldedit;
1531    }
1532
1533    public @NonNull File getConfigFile() {
1534        return this.configFile;
1535    }
1536
1537    public @NonNull File getWorldsFile() {
1538        return this.worldsFile;
1539    }
1540
1541    public @NonNull YamlConfiguration getWorldConfiguration() {
1542        return this.worldConfiguration;
1543    }
1544
1545    /**
1546     * Get the caption map belonging to a namespace. If none exists, a dummy
1547     * caption map will be returned.
1548     * <p>
1549     * You can register a caption map by calling {@link #registerCaptionMap(String, CaptionMap)}
1550     * </p>
1551     *
1552     * @param namespace Namespace
1553     * @return Map instance
1554     */
1555    public @NonNull CaptionMap getCaptionMap(final @NonNull String namespace) {
1556        return this.captionMaps.computeIfAbsent(
1557                namespace.toLowerCase(Locale.ENGLISH),
1558                missingNamespace -> new DummyCaptionMap()
1559        );
1560    }
1561
1562    /**
1563     * Register a caption map. The namespace needs to be equal to the namespace used for
1564     * the {@link TranslatableCaption}s inside the map.
1565     *
1566     * @param namespace  Namespace
1567     * @param captionMap Map instance
1568     */
1569    public void registerCaptionMap(
1570            final @NonNull String namespace,
1571            final @NonNull CaptionMap captionMap
1572    ) {
1573        if (namespace.equalsIgnoreCase(TranslatableCaption.DEFAULT_NAMESPACE)) {
1574            throw new IllegalArgumentException("Cannot replace default caption map");
1575        }
1576        this.captionMaps.put(namespace.toLowerCase(Locale.ENGLISH), captionMap);
1577    }
1578
1579    public @NonNull EventDispatcher getEventDispatcher() {
1580        return this.eventDispatcher;
1581    }
1582
1583    public @NonNull PlotListener getPlotListener() {
1584        return this.plotListener;
1585    }
1586
1587    /**
1588     * Get if the {@link PlatformReadyEvent} has been sent by WorldEdit. There is no way to query this within WorldEdit itself.
1589     */
1590    public boolean isWeInitialised() {
1591        return weInitialised;
1592    }
1593
1594    /**
1595     * Different ways of sorting {@link Plot plots}
1596     */
1597    public enum SortType {
1598        /**
1599         * Sort plots by their creation, using their index in the database
1600         */
1601        CREATION_DATE,
1602        /**
1603         * Sort plots by their creation timestamp
1604         */
1605        CREATION_DATE_TIMESTAMP,
1606        /**
1607         * Sort plots by when they were last modified
1608         */
1609        LAST_MODIFIED,
1610        /**
1611         * Sort plots based on their distance from the origin of the world
1612         */
1613        DISTANCE_FROM_ORIGIN
1614    }
1615
1616    private final class WEPlatformReadyListener {
1617
1618        @SuppressWarnings("unused")
1619        @Subscribe(priority = EventHandler.Priority.VERY_EARLY)
1620        public void onPlatformReady(PlatformReadyEvent event) {
1621            weInitialised = true;
1622            WorldEdit.getInstance().getEventBus().unregister(WEPlatformReadyListener.this);
1623        }
1624
1625    }
1626
1627}