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