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