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.bukkit;
020
021import com.google.inject.Guice;
022import com.google.inject.Inject;
023import com.google.inject.Injector;
024import com.google.inject.Key;
025import com.google.inject.Singleton;
026import com.google.inject.Stage;
027import com.google.inject.TypeLiteral;
028import com.plotsquared.bukkit.generator.BukkitPlotGenerator;
029import com.plotsquared.bukkit.inject.BackupModule;
030import com.plotsquared.bukkit.inject.BukkitModule;
031import com.plotsquared.bukkit.inject.PermissionModule;
032import com.plotsquared.bukkit.inject.WorldManagerModule;
033import com.plotsquared.bukkit.listener.BlockEventListener;
034import com.plotsquared.bukkit.listener.BlockEventListener117;
035import com.plotsquared.bukkit.listener.ChunkListener;
036import com.plotsquared.bukkit.listener.EntityEventListener;
037import com.plotsquared.bukkit.listener.EntitySpawnListener;
038import com.plotsquared.bukkit.listener.PaperListener;
039import com.plotsquared.bukkit.listener.PlayerEventListener;
040import com.plotsquared.bukkit.listener.ProjectileEventListener;
041import com.plotsquared.bukkit.listener.ServerListener;
042import com.plotsquared.bukkit.listener.SingleWorldListener;
043import com.plotsquared.bukkit.listener.SpigotListener;
044import com.plotsquared.bukkit.listener.WorldEvents;
045import com.plotsquared.bukkit.placeholder.PAPIPlaceholders;
046import com.plotsquared.bukkit.placeholder.PlaceholderFormatter;
047import com.plotsquared.bukkit.player.BukkitPlayer;
048import com.plotsquared.bukkit.player.BukkitPlayerManager;
049import com.plotsquared.bukkit.util.BukkitUtil;
050import com.plotsquared.bukkit.util.BukkitWorld;
051import com.plotsquared.bukkit.util.SetGenCB;
052import com.plotsquared.bukkit.util.UpdateUtility;
053import com.plotsquared.bukkit.util.task.BukkitTaskManager;
054import com.plotsquared.bukkit.util.task.PaperTimeConverter;
055import com.plotsquared.bukkit.util.task.SpigotTimeConverter;
056import com.plotsquared.bukkit.uuid.EssentialsUUIDService;
057import com.plotsquared.bukkit.uuid.LuckPermsUUIDService;
058import com.plotsquared.bukkit.uuid.OfflinePlayerUUIDService;
059import com.plotsquared.bukkit.uuid.PaperUUIDService;
060import com.plotsquared.bukkit.uuid.SQLiteUUIDService;
061import com.plotsquared.bukkit.uuid.SquirrelIdUUIDService;
062import com.plotsquared.core.PlotPlatform;
063import com.plotsquared.core.PlotSquared;
064import com.plotsquared.core.backup.BackupManager;
065import com.plotsquared.core.components.ComponentPresetManager;
066import com.plotsquared.core.configuration.ConfigurationNode;
067import com.plotsquared.core.configuration.ConfigurationSection;
068import com.plotsquared.core.configuration.ConfigurationUtil;
069import com.plotsquared.core.configuration.Settings;
070import com.plotsquared.core.configuration.Storage;
071import com.plotsquared.core.configuration.caption.ChatFormatter;
072import com.plotsquared.core.configuration.file.YamlConfiguration;
073import com.plotsquared.core.database.DBFunc;
074import com.plotsquared.core.events.RemoveRoadEntityEvent;
075import com.plotsquared.core.events.Result;
076import com.plotsquared.core.generator.GeneratorWrapper;
077import com.plotsquared.core.generator.IndependentPlotGenerator;
078import com.plotsquared.core.generator.SingleWorldGenerator;
079import com.plotsquared.core.inject.annotations.BackgroundPipeline;
080import com.plotsquared.core.inject.annotations.DefaultGenerator;
081import com.plotsquared.core.inject.annotations.ImpromptuPipeline;
082import com.plotsquared.core.inject.annotations.WorldConfig;
083import com.plotsquared.core.inject.annotations.WorldFile;
084import com.plotsquared.core.inject.modules.PlotSquaredModule;
085import com.plotsquared.core.listener.PlotListener;
086import com.plotsquared.core.listener.WESubscriber;
087import com.plotsquared.core.player.PlotPlayer;
088import com.plotsquared.core.plot.Plot;
089import com.plotsquared.core.plot.PlotArea;
090import com.plotsquared.core.plot.PlotAreaTerrainType;
091import com.plotsquared.core.plot.PlotAreaType;
092import com.plotsquared.core.plot.PlotId;
093import com.plotsquared.core.plot.comment.CommentManager;
094import com.plotsquared.core.plot.flag.implementations.ServerPlotFlag;
095import com.plotsquared.core.plot.world.PlotAreaManager;
096import com.plotsquared.core.plot.world.SinglePlotArea;
097import com.plotsquared.core.plot.world.SinglePlotAreaManager;
098import com.plotsquared.core.setup.PlotAreaBuilder;
099import com.plotsquared.core.setup.SettingsNodesWrapper;
100import com.plotsquared.core.util.EventDispatcher;
101import com.plotsquared.core.util.FileUtils;
102import com.plotsquared.core.util.PlatformWorldManager;
103import com.plotsquared.core.util.PlayerManager;
104import com.plotsquared.core.util.PremiumVerification;
105import com.plotsquared.core.util.ReflectionUtils;
106import com.plotsquared.core.util.SetupUtils;
107import com.plotsquared.core.util.WorldUtil;
108import com.plotsquared.core.util.task.TaskManager;
109import com.plotsquared.core.util.task.TaskTime;
110import com.plotsquared.core.uuid.CacheUUIDService;
111import com.plotsquared.core.uuid.UUIDPipeline;
112import com.plotsquared.core.uuid.offline.OfflineModeUUIDService;
113import com.sk89q.worldedit.WorldEdit;
114import com.sk89q.worldedit.bukkit.BukkitAdapter;
115import io.papermc.lib.PaperLib;
116import net.kyori.adventure.audience.Audience;
117import net.kyori.adventure.text.Component;
118import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
119import org.apache.logging.log4j.LogManager;
120import org.apache.logging.log4j.Logger;
121import org.bstats.bukkit.Metrics;
122import org.bstats.charts.DrilldownPie;
123import org.bstats.charts.SimplePie;
124import org.bukkit.Bukkit;
125import org.bukkit.Chunk;
126import org.bukkit.Location;
127import org.bukkit.World;
128import org.bukkit.command.PluginCommand;
129import org.bukkit.entity.Entity;
130import org.bukkit.entity.LivingEntity;
131import org.bukkit.entity.Player;
132import org.bukkit.event.Listener;
133import org.bukkit.generator.ChunkGenerator;
134import org.bukkit.metadata.FixedMetadataValue;
135import org.bukkit.metadata.MetadataValue;
136import org.bukkit.plugin.Plugin;
137import org.bukkit.plugin.java.JavaPlugin;
138import org.checkerframework.checker.nullness.qual.NonNull;
139import org.checkerframework.checker.nullness.qual.Nullable;
140import org.incendo.serverlib.ServerLib;
141
142import java.io.File;
143import java.lang.reflect.Method;
144import java.util.ArrayList;
145import java.util.Arrays;
146import java.util.Collections;
147import java.util.Comparator;
148import java.util.HashMap;
149import java.util.HashSet;
150import java.util.Iterator;
151import java.util.List;
152import java.util.Locale;
153import java.util.Map;
154import java.util.Queue;
155import java.util.Set;
156import java.util.UUID;
157import java.util.concurrent.ExecutionException;
158import java.util.concurrent.Executors;
159import java.util.concurrent.LinkedBlockingQueue;
160import java.util.concurrent.TimeUnit;
161
162import static com.plotsquared.core.util.PremiumVerification.getDownloadID;
163import static com.plotsquared.core.util.PremiumVerification.getResourceID;
164import static com.plotsquared.core.util.PremiumVerification.getUserID;
165import static com.plotsquared.core.util.ReflectionUtils.getRefClass;
166
167@SuppressWarnings("unused")
168@Singleton
169public final class BukkitPlatform extends JavaPlugin implements Listener, PlotPlatform<Player> {
170
171    private static final Logger LOGGER = LogManager.getLogger("PlotSquared/" + BukkitPlatform.class.getSimpleName());
172    private static final int BSTATS_ID = 1404;
173
174    static {
175        try {
176            Settings.load(new File(PlotSquared.platform().getDirectory(), "settings.yml"));
177        } catch (Throwable ignored) {
178        }
179    }
180
181    private int[] version;
182    private String pluginName;
183    private SingleWorldListener singleWorldListener;
184    private Method methodUnloadChunk0;
185    private boolean methodUnloadSetup = false;
186    private boolean metricsStarted;
187    private boolean faweHook = false;
188
189    private Injector injector;
190
191    @Inject
192    private PlotAreaManager plotAreaManager;
193    @Inject
194    private EventDispatcher eventDispatcher;
195    @Inject
196    private PlotListener plotListener;
197    @Inject
198    @WorldConfig
199    private YamlConfiguration worldConfiguration;
200    @Inject
201    @WorldFile
202    private File worldfile;
203    @Inject
204    private BukkitPlayerManager playerManager;
205    @Inject
206    private BackupManager backupManager;
207    @Inject
208    @ImpromptuPipeline
209    private UUIDPipeline impromptuPipeline;
210    @Inject
211    @BackgroundPipeline
212    private UUIDPipeline backgroundPipeline;
213    @Inject
214    private PlatformWorldManager<World> worldManager;
215    private Locale serverLocale;
216
217    @SuppressWarnings("StringSplitter")
218    @Override
219    public int @NonNull [] serverVersion() {
220        if (this.version == null) {
221            try {
222                this.version = new int[3];
223                String[] split = Bukkit.getBukkitVersion().split("-")[0].split("\\.");
224                this.version[0] = Integer.parseInt(split[0]);
225                this.version[1] = Integer.parseInt(split[1]);
226                if (split.length == 3) {
227                    this.version[2] = Integer.parseInt(split[2]);
228                }
229            } catch (NumberFormatException e) {
230                e.printStackTrace();
231                return new int[]{1, 13, 0};
232            }
233        }
234        return this.version;
235    }
236
237    @Override
238    public int versionMinHeight() {
239        return serverVersion()[1] >= 18 ? -64 : 0;
240    }
241
242    @Override
243    public int versionMaxHeight() {
244        return serverVersion()[1] >= 18 ? 319 : 255;
245    }
246
247    @Override
248    public @NonNull String serverImplementation() {
249        return Bukkit.getVersion();
250    }
251
252    @Override
253    public void onEnable() {
254        this.pluginName = getDescription().getName();
255
256        final TaskTime.TimeConverter timeConverter;
257        if (PaperLib.isPaper()) {
258            timeConverter = new PaperTimeConverter();
259        } else {
260            timeConverter = new SpigotTimeConverter();
261        }
262
263        // Stuff that needs to be created before the PlotSquared instance
264        PlotPlayer.registerConverter(Player.class, BukkitUtil::adapt);
265        TaskManager.setPlatformImplementation(new BukkitTaskManager(this, timeConverter));
266
267        final PlotSquared plotSquared = new PlotSquared(this, "Bukkit");
268
269        // FastAsyncWorldEdit
270        if (Settings.FAWE_Components.FAWE_HOOK) {
271            Plugin fawe = getServer().getPluginManager().getPlugin("FastAsyncWorldEdit");
272            if (fawe != null) {
273                try {
274                    Class.forName("com.fastasyncworldedit.bukkit.regions.plotsquared.FaweQueueCoordinator");
275                    faweHook = true;
276                } catch (Exception ignored) {
277                    LOGGER.error("Incompatible version of FastAsyncWorldEdit to enable hook, please upgrade: https://ci.athion" +
278                            ".net/job/FastAsyncWorldEdit/");
279                }
280            }
281        }
282
283        // We create the injector after PlotSquared has been initialized, so that we have access
284        // to generated instances and settings
285        this.injector = Guice
286                .createInjector(
287                        Stage.PRODUCTION,
288                        new PermissionModule(),
289                        new WorldManagerModule(),
290                        new PlotSquaredModule(),
291                        new BukkitModule(this),
292                        new BackupModule()
293                );
294        this.injector.injectMembers(this);
295
296        this.serverLocale = Locale.forLanguageTag(Settings.Enabled_Components.DEFAULT_LOCALE);
297
298        if (PremiumVerification.isPremium() && Settings.Enabled_Components.UPDATE_NOTIFICATIONS) {
299            injector.getInstance(UpdateUtility.class).updateChecker();
300        }
301
302        if (PremiumVerification.isPremium()) {
303            LOGGER.info("PlotSquared version licensed to Spigot user {}", getUserID());
304            LOGGER.info("https://www.spigotmc.org/resources/{}", getResourceID());
305            LOGGER.info("Download ID: {}", getDownloadID());
306            LOGGER.info("Thanks for supporting us :)");
307        } else {
308            LOGGER.info("Couldn't verify purchase :(");
309        }
310
311        // Database
312        if (Settings.Enabled_Components.DATABASE) {
313            plotSquared.setupDatabase();
314        }
315
316        // Check if we need to convert old flag values, etc
317        if (!plotSquared.getConfigurationVersion().equalsIgnoreCase("v5")) {
318            // Perform upgrade
319            if (DBFunc.dbManager.convertFlags()) {
320                LOGGER.info("Flags were converted successfully!");
321                // Update the config version
322                try {
323                    plotSquared.setConfigurationVersion("v5");
324                } catch (final Exception e) {
325                    e.printStackTrace();
326                }
327            }
328        }
329
330        // Comments
331        CommentManager.registerDefaultInboxes();
332
333        // Do stuff that was previously done in PlotSquared
334        // Kill entities
335        if (Settings.Enabled_Components.KILL_ROAD_MOBS || Settings.Enabled_Components.KILL_ROAD_VEHICLES) {
336            this.runEntityTask();
337        }
338
339        // WorldEdit
340        if (Settings.Enabled_Components.WORLDEDIT_RESTRICTIONS) {
341            try {
342                WorldEdit.getInstance().getEventBus().register(this.injector().getInstance(WESubscriber.class));
343                LOGGER.info("{} hooked into WorldEdit", this.pluginName());
344            } catch (Throwable e) {
345                LOGGER.error(
346                        "Incompatible version of WorldEdit, please upgrade: https://builds.enginehub.org/job/worldedit?branch=master");
347            }
348        }
349
350        if (Settings.Enabled_Components.EVENTS) {
351            getServer().getPluginManager().registerEvents(injector().getInstance(PlayerEventListener.class), this);
352            getServer().getPluginManager().registerEvents(injector().getInstance(BlockEventListener.class), this);
353            if (serverVersion()[1] >= 17) {
354                getServer().getPluginManager().registerEvents(injector().getInstance(BlockEventListener117.class), this);
355            }
356            getServer().getPluginManager().registerEvents(injector().getInstance(EntityEventListener.class), this);
357            getServer().getPluginManager().registerEvents(injector().getInstance(ProjectileEventListener.class), this);
358            getServer().getPluginManager().registerEvents(injector().getInstance(ServerListener.class), this);
359            getServer().getPluginManager().registerEvents(injector().getInstance(EntitySpawnListener.class), this);
360            if (PaperLib.isPaper() && Settings.Paper_Components.PAPER_LISTENERS) {
361                    getServer().getPluginManager().registerEvents(injector().getInstance(PaperListener.class), this);
362            } else {
363                getServer().getPluginManager().registerEvents(injector().getInstance(SpigotListener.class), this);
364            }
365            this.plotListener.startRunnable();
366        }
367
368        // Required
369        getServer().getPluginManager().registerEvents(injector().getInstance(WorldEvents.class), this);
370        if (Settings.Enabled_Components.CHUNK_PROCESSOR) {
371            getServer().getPluginManager().registerEvents(injector().getInstance(ChunkListener.class), this);
372        }
373
374        // Commands
375        if (Settings.Enabled_Components.COMMANDS) {
376            this.registerCommands();
377        }
378
379        // Permissions
380        this.permissionHandler().initialize();
381
382        if (Settings.Enabled_Components.COMPONENT_PRESETS) {
383            try {
384                injector().getInstance(ComponentPresetManager.class);
385            } catch (final Exception e) {
386                LOGGER.error("Failed to initialize the preset system", e);
387            }
388        }
389
390        // World generators:
391        final ConfigurationSection section = this.worldConfiguration.getConfigurationSection("worlds");
392        final WorldUtil worldUtil = injector().getInstance(WorldUtil.class);
393
394        if (section != null) {
395            for (String world : section.getKeys(false)) {
396                if (world.equals("CheckingPlotSquaredGenerator")) {
397                    continue;
398                }
399                if (worldUtil.isWorld(world)) {
400                    this.setGenerator(world);
401                }
402            }
403            TaskManager.runTaskLater(() -> {
404                for (String world : section.getKeys(false)) {
405                    if (world.equals("CheckingPlotSquaredGenerator")) {
406                        continue;
407                    }
408                    if (!worldUtil.isWorld(world) && !world.equals("*")) {
409                        if (Settings.DEBUG) {
410                            LOGGER.warn(
411                                    "`{}` was not properly loaded - {} will now try to load it properly",
412                                    world,
413                                    this.pluginName()
414                            );
415                            LOGGER.warn(
416                                    "- Are you trying to delete this world? Remember to remove it from the worlds.yml, bukkit.yml and multiverse worlds.yml");
417                            LOGGER.warn("- Your world management plugin may be faulty (or non existent)");
418                            LOGGER.warn("- The named world is not a plot world");
419                            LOGGER.warn("This message may also be a false positive and could be ignored.");
420                        }
421                        this.setGenerator(world);
422                    }
423                }
424            }, TaskTime.ticks(1L));
425        }
426
427        plotSquared.startExpiryTasks();
428
429        // Once the server has loaded force updating all generators known to PlotSquared
430        TaskManager.runTaskLater(() -> PlotSquared.platform().setupUtils().updateGenerators(true), TaskTime.ticks(1L));
431
432        // Services are accessed in order
433        final CacheUUIDService cacheUUIDService = new CacheUUIDService(Settings.UUID.UUID_CACHE_SIZE);
434        this.impromptuPipeline.registerService(cacheUUIDService);
435        this.backgroundPipeline.registerService(cacheUUIDService);
436        this.impromptuPipeline.registerConsumer(cacheUUIDService);
437        this.backgroundPipeline.registerConsumer(cacheUUIDService);
438
439        // Now, if the server is in offline mode we can only use profiles and direct UUID
440        // access, and so we skip the player profile stuff as well as SquirrelID (Mojang lookups)
441        if (Settings.UUID.OFFLINE) {
442            final OfflineModeUUIDService offlineModeUUIDService = new OfflineModeUUIDService();
443            this.impromptuPipeline.registerService(offlineModeUUIDService);
444            this.backgroundPipeline.registerService(offlineModeUUIDService);
445            LOGGER.info("(UUID) Using the offline mode UUID service");
446        }
447
448        if (Settings.UUID.SERVICE_BUKKIT) {
449            final OfflinePlayerUUIDService offlinePlayerUUIDService = new OfflinePlayerUUIDService();
450            this.impromptuPipeline.registerService(offlinePlayerUUIDService);
451            this.backgroundPipeline.registerService(offlinePlayerUUIDService);
452        }
453
454        final SQLiteUUIDService sqLiteUUIDService = new SQLiteUUIDService("user_cache.db");
455
456        final SQLiteUUIDService legacyUUIDService;
457        if (Settings.UUID.LEGACY_DATABASE_SUPPORT && FileUtils
458                .getFile(PlotSquared.platform().getDirectory(), "usercache.db")
459                .exists()) {
460            legacyUUIDService = new SQLiteUUIDService("usercache.db");
461        } else {
462            legacyUUIDService = null;
463        }
464
465        final LuckPermsUUIDService luckPermsUUIDService;
466        if (Settings.UUID.SERVICE_LUCKPERMS && Bukkit.getPluginManager().getPlugin("LuckPerms") != null) {
467            luckPermsUUIDService = new LuckPermsUUIDService();
468            LOGGER.info("(UUID) Using LuckPerms as a complementary UUID service");
469        } else {
470            luckPermsUUIDService = null;
471        }
472
473        final EssentialsUUIDService essentialsUUIDService;
474        if (Settings.UUID.SERVICE_ESSENTIALSX && Bukkit.getPluginManager().getPlugin("Essentials") != null) {
475            essentialsUUIDService = new EssentialsUUIDService();
476            LOGGER.info("(UUID) Using EssentialsX as a complementary UUID service");
477        } else {
478            essentialsUUIDService = null;
479        }
480
481        if (!Settings.UUID.OFFLINE) {
482            // If running Paper we'll also try to use their profiles
483            if (Bukkit.getOnlineMode() && PaperLib.isPaper() && Settings.UUID.SERVICE_PAPER) {
484                final PaperUUIDService paperUUIDService = new PaperUUIDService();
485                this.impromptuPipeline.registerService(paperUUIDService);
486                this.backgroundPipeline.registerService(paperUUIDService);
487                LOGGER.info("(UUID) Using Paper as a complementary UUID service");
488            }
489
490            this.impromptuPipeline.registerService(sqLiteUUIDService);
491            this.backgroundPipeline.registerService(sqLiteUUIDService);
492            this.impromptuPipeline.registerConsumer(sqLiteUUIDService);
493            this.backgroundPipeline.registerConsumer(sqLiteUUIDService);
494
495            if (legacyUUIDService != null) {
496                this.impromptuPipeline.registerService(legacyUUIDService);
497                this.backgroundPipeline.registerService(legacyUUIDService);
498            }
499
500            // Plugin providers
501            if (luckPermsUUIDService != null) {
502                this.impromptuPipeline.registerService(luckPermsUUIDService);
503                this.backgroundPipeline.registerService(luckPermsUUIDService);
504            }
505            if (essentialsUUIDService != null) {
506                this.impromptuPipeline.registerService(essentialsUUIDService);
507                this.backgroundPipeline.registerService(essentialsUUIDService);
508            }
509
510            if (Settings.UUID.IMPROMPTU_SERVICE_MOJANG_API) {
511                final SquirrelIdUUIDService impromptuMojangService = new SquirrelIdUUIDService(Settings.UUID.IMPROMPTU_LIMIT);
512                this.impromptuPipeline.registerService(impromptuMojangService);
513            }
514            final SquirrelIdUUIDService backgroundMojangService = new SquirrelIdUUIDService(Settings.UUID.BACKGROUND_LIMIT);
515            this.backgroundPipeline.registerService(backgroundMojangService);
516        } else {
517            this.impromptuPipeline.registerService(sqLiteUUIDService);
518            this.backgroundPipeline.registerService(sqLiteUUIDService);
519            this.impromptuPipeline.registerConsumer(sqLiteUUIDService);
520            this.backgroundPipeline.registerConsumer(sqLiteUUIDService);
521
522            if (legacyUUIDService != null) {
523                this.impromptuPipeline.registerService(legacyUUIDService);
524                this.backgroundPipeline.registerService(legacyUUIDService);
525            }
526        }
527
528        this.impromptuPipeline.storeImmediately("*", DBFunc.EVERYONE);
529
530        if (Settings.UUID.BACKGROUND_CACHING_ENABLED) {
531            this.startUuidCaching(sqLiteUUIDService, cacheUUIDService);
532        }
533
534        if (Bukkit.getPluginManager().getPlugin("PlaceholderAPI") != null) {
535            injector.getInstance(PAPIPlaceholders.class).register();
536            if (Settings.Enabled_Components.EXTERNAL_PLACEHOLDERS) {
537                ChatFormatter.formatters.add(injector().getInstance(PlaceholderFormatter.class));
538            }
539            LOGGER.info("PlotSquared hooked into PlaceholderAPI");
540        }
541
542        this.startMetrics();
543
544        if (Settings.Enabled_Components.WORLDS) {
545            TaskManager.getPlatformImplementation().taskRepeat(this::unload, TaskTime.seconds(1L));
546            try {
547                singleWorldListener = injector().getInstance(SingleWorldListener.class);
548                Bukkit.getPluginManager().registerEvents(singleWorldListener, this);
549            } catch (Exception e) {
550                e.printStackTrace();
551            }
552        }
553
554        // Clean up potential memory leak
555        Bukkit.getScheduler().runTaskTimer(this, () -> {
556            try {
557                for (final PlotPlayer<? extends Player> player : this.playerManager().getPlayers()) {
558                    if (player.getPlatformPlayer() == null || !player.getPlatformPlayer().isOnline()) {
559                        this.playerManager().removePlayer(player);
560                    }
561                }
562            } catch (final Exception e) {
563                getLogger().warning("Failed to clean up players: " + e.getMessage());
564            }
565        }, 100L, 100L);
566
567        // Check if we are in a safe environment
568        ServerLib.checkUnsafeForks();
569    }
570
571    private void unload() {
572        if (!this.methodUnloadSetup) {
573            this.methodUnloadSetup = true;
574            try {
575                ReflectionUtils.RefClass classCraftWorld = getRefClass("{cb}.CraftWorld");
576                this.methodUnloadChunk0 = classCraftWorld.getRealClass().getDeclaredMethod(
577                        "unloadChunk0",
578                        int.class,
579                        int.class,
580                        boolean.class
581                );
582                this.methodUnloadChunk0.setAccessible(true);
583            } catch (Throwable event) {
584                event.printStackTrace();
585            }
586        }
587
588        if (this.plotAreaManager instanceof SinglePlotAreaManager) {
589            long start = System.currentTimeMillis();
590            final SinglePlotArea area = ((SinglePlotAreaManager) this.plotAreaManager).getArea();
591
592            outer:
593            for (final World world : Bukkit.getWorlds()) {
594                final String name = world.getName();
595                final char char0 = name.charAt(0);
596                if (!Character.isDigit(char0) && char0 != '-') {
597                    continue;
598                }
599
600                if (!world.getPlayers().isEmpty()) {
601                    continue;
602                }
603
604                PlotId id;
605                try {
606                    id = PlotId.fromString(name);
607                } catch (IllegalArgumentException ignored) {
608                    continue;
609                }
610                final Plot plot = area.getOwnedPlot(id);
611                if (plot != null) {
612                    if (!plot.getFlag(ServerPlotFlag.class) || PlotSquared
613                            .platform()
614                            .playerManager()
615                            .getPlayerIfExists(plot.getOwner()) == null) {
616                        if (world.getKeepSpawnInMemory()) {
617                            world.setKeepSpawnInMemory(false);
618                            return;
619                        }
620                        final Chunk[] chunks = world.getLoadedChunks();
621                        if (chunks.length == 0) {
622                            if (!Bukkit.unloadWorld(world, true)) {
623                                LOGGER.warn("Failed to unload {}", world.getName());
624                            }
625                            return;
626                        } else {
627                            int index = 0;
628                            do {
629                                final Chunk chunkI = chunks[index++];
630                                boolean result;
631                                if (methodUnloadChunk0 != null) {
632                                    try {
633                                        result = (boolean) methodUnloadChunk0.invoke(world, chunkI.getX(), chunkI.getZ(), true);
634                                    } catch (Throwable e) {
635                                        methodUnloadChunk0 = null;
636                                        e.printStackTrace();
637                                        continue outer;
638                                    }
639                                } else {
640                                    result = world.unloadChunk(chunkI.getX(), chunkI.getZ(), true);
641                                }
642                                if (!result) {
643                                    continue outer;
644                                }
645                                if (System.currentTimeMillis() - start > 5) {
646                                    return;
647                                }
648                            } while (index < chunks.length);
649                        }
650                    }
651                }
652            }
653        }
654    }
655
656    private void startUuidCaching(
657            final @NonNull SQLiteUUIDService sqLiteUUIDService,
658            final @NonNull CacheUUIDService cacheUUIDService
659    ) {
660        // Record all unique UUID's and put them into a queue
661        final Set<UUID> uuidSet = new HashSet<>();
662        PlotSquared.get().forEachPlotRaw(plot -> {
663            uuidSet.add(plot.getOwnerAbs());
664            uuidSet.addAll(plot.getMembers());
665            uuidSet.addAll(plot.getTrusted());
666            uuidSet.addAll(plot.getDenied());
667        });
668        final Queue<UUID> uuidQueue = new LinkedBlockingQueue<>(uuidSet);
669
670        LOGGER.info("(UUID) {} UUIDs will be cached", uuidQueue.size());
671
672        Executors.newSingleThreadScheduledExecutor().schedule(() -> {
673            // Begin by reading all the SQLite cache at once
674            cacheUUIDService.accept(sqLiteUUIDService.getAll());
675            // Now fetch names for all known UUIDs
676            final int totalSize = uuidQueue.size();
677            int read = 0;
678            LOGGER.info("(UUID) PlotSquared will fetch UUIDs in groups of {}", Settings.UUID.BACKGROUND_LIMIT);
679            final List<UUID> uuidList = new ArrayList<>(Settings.UUID.BACKGROUND_LIMIT);
680
681            // Used to indicate that the second retrieval has been attempted
682            boolean secondRun = false;
683
684            while (!uuidQueue.isEmpty() || !uuidList.isEmpty()) {
685                if (!uuidList.isEmpty() && secondRun) {
686                    LOGGER.warn("(UUID) Giving up on last batch. Fetching new batch instead");
687                    uuidList.clear();
688                }
689                if (uuidList.isEmpty()) {
690                    // Retrieve the secondRun variable to indicate that we're retrieving a
691                    // fresh batch
692                    secondRun = false;
693                    // Populate the request list
694                    for (int i = 0; i < Settings.UUID.BACKGROUND_LIMIT && !uuidQueue.isEmpty(); i++) {
695                        uuidList.add(uuidQueue.poll());
696                        read++;
697                    }
698                } else {
699                    // If the list isn't empty then this is a second run for
700                    // an old batch, so we re-use the patch
701                    secondRun = true;
702                }
703                try {
704                    PlotSquared.get().getBackgroundUUIDPipeline().getNames(uuidList).get();
705                    // Clear the list if we successfully index all the names
706                    uuidList.clear();
707                    // Print progress
708                    final double percentage = ((double) read / (double) totalSize) * 100.0D;
709                    if (Settings.DEBUG) {
710                        LOGGER.info("(UUID) PlotSquared has cached {} of UUIDs", String.format("%.1f%%", percentage));
711                    }
712                } catch (final InterruptedException | ExecutionException e) {
713                    LOGGER.error("(UUID) Failed to retrieve last batch. Will try again", e);
714                }
715            }
716            LOGGER.info("(UUID) PlotSquared has cached all UUIDs");
717        }, 10, TimeUnit.SECONDS);
718    }
719
720    @Override
721    public void onDisable() {
722        PlotSquared.get().disable();
723        Bukkit.getScheduler().cancelTasks(this);
724    }
725
726    @Override
727    public void shutdown() {
728        this.getServer().getPluginManager().disablePlugin(this);
729    }
730
731    @Override
732    public void shutdownServer() {
733        getServer().shutdown();
734    }
735
736    private void registerCommands() {
737        final BukkitCommand bukkitCommand = new BukkitCommand();
738        final PluginCommand plotCommand = getCommand("plots");
739        if (plotCommand != null) {
740            plotCommand.setExecutor(bukkitCommand);
741            plotCommand.setAliases(Arrays.asList("p", "ps", "plotme", "plot"));
742            plotCommand.setTabCompleter(bukkitCommand);
743        }
744    }
745
746    @Override
747    public @NonNull File getDirectory() {
748        return getDataFolder();
749    }
750
751    @Override
752    public @NonNull File worldContainer() {
753        return Bukkit.getWorldContainer();
754    }
755
756    @SuppressWarnings("deprecation")
757    private void runEntityTask() {
758        TaskManager.runTaskRepeat(() -> this.plotAreaManager.forEachPlotArea(plotArea -> {
759            final World world = Bukkit.getWorld(plotArea.getWorldName());
760            try {
761                if (world == null) {
762                    return;
763                }
764                List<Entity> entities = world.getEntities();
765                Iterator<Entity> iterator = entities.iterator();
766                while (iterator.hasNext()) {
767                    Entity entity = iterator.next();
768                    switch (entity.getType().toString()) {
769                        case "EGG":
770                        case "FISHING_HOOK":
771                        case "ENDER_SIGNAL":
772                        case "AREA_EFFECT_CLOUD":
773                        case "EXPERIENCE_ORB":
774                        case "LEASH_HITCH":
775                        case "FIREWORK":
776                        case "LIGHTNING":
777                        case "WITHER_SKULL":
778                        case "UNKNOWN":
779                        case "PLAYER":
780                            // non moving / unmovable
781                            continue;
782                        case "THROWN_EXP_BOTTLE":
783                        case "SPLASH_POTION":
784                        case "SNOWBALL":
785                        case "SHULKER_BULLET":
786                        case "SPECTRAL_ARROW":
787                        case "ENDER_PEARL":
788                        case "ARROW":
789                        case "LLAMA_SPIT":
790                        case "TRIDENT":
791                            // managed elsewhere | projectile
792                            continue;
793                        case "ITEM_FRAME":
794                        case "PAINTING":
795                            // Not vehicles
796                            continue;
797                        case "ARMOR_STAND":
798                            // Temporarily classify as vehicle
799                        case "MINECART":
800                        case "MINECART_CHEST":
801                        case "MINECART_COMMAND":
802                        case "MINECART_FURNACE":
803                        case "MINECART_HOPPER":
804                        case "MINECART_MOB_SPAWNER":
805                        case "ENDER_CRYSTAL":
806                        case "MINECART_TNT":
807                        case "BOAT":
808                            if (Settings.Enabled_Components.KILL_ROAD_VEHICLES) {
809                                com.plotsquared.core.location.Location location = BukkitUtil.adapt(entity.getLocation());
810                                Plot plot = location.getPlot();
811                                if (plot == null) {
812                                    if (location.isPlotArea()) {
813                                        if (entity.hasMetadata("ps-tmp-teleport")) {
814                                            continue;
815                                        }
816                                        this.removeRoadEntity(entity, iterator);
817                                    }
818                                    continue;
819                                }
820                                List<MetadataValue> meta = entity.getMetadata("plot");
821                                if (meta.isEmpty()) {
822                                    continue;
823                                }
824                                Plot origin = (Plot) meta.get(0).value();
825                                if (!plot.equals(origin.getBasePlot(false))) {
826                                    if (entity.hasMetadata("ps-tmp-teleport")) {
827                                        continue;
828                                    }
829                                    this.removeRoadEntity(entity, iterator);
830                                }
831                            }
832                            continue;
833                        case "SMALL_FIREBALL":
834                        case "FIREBALL":
835                        case "DRAGON_FIREBALL":
836                        case "DROPPED_ITEM":
837                            if (Settings.Enabled_Components.KILL_ROAD_ITEMS
838                                    && plotArea.getOwnedPlotAbs(BukkitUtil.adapt(entity.getLocation())) == null) {
839                                this.removeRoadEntity(entity, iterator);
840                            }
841                            // dropped item
842                            continue;
843                        case "PRIMED_TNT":
844                        case "FALLING_BLOCK":
845                            // managed elsewhere
846                            continue;
847                        case "SHULKER":
848                            if (Settings.Enabled_Components.KILL_ROAD_MOBS && (Settings.Enabled_Components.KILL_NAMED_ROAD_MOBS || entity.getCustomName() == null)) {
849                                LivingEntity livingEntity = (LivingEntity) entity;
850                                List<MetadataValue> meta = entity.getMetadata("shulkerPlot");
851                                if (!meta.isEmpty()) {
852                                    if (livingEntity.isLeashed() && !Settings.Enabled_Components.KILL_OWNED_ROAD_MOBS) {
853                                        continue;
854                                    }
855                                    List<MetadataValue> keep = entity.getMetadata("keep");
856                                    if (!keep.isEmpty()) {
857                                        continue;
858                                    }
859
860                                    PlotId originalPlotId = (PlotId) meta.get(0).value();
861                                    if (originalPlotId != null) {
862                                        com.plotsquared.core.location.Location pLoc = BukkitUtil.adapt(entity.getLocation());
863                                        PlotArea area = pLoc.getPlotArea();
864                                        if (area != null) {
865                                            Plot currentPlot = area.getPlotAbs(pLoc);
866                                            if (currentPlot == null || !originalPlotId.equals(currentPlot.getId())) {
867                                                if (entity.hasMetadata("ps-tmp-teleport")) {
868                                                    continue;
869                                                }
870                                                this.removeRoadEntity(entity, iterator);
871                                            }
872                                        }
873                                    }
874                                } else {
875                                    //This is to apply the metadata to already spawned shulkers (see EntitySpawnListener.java)
876                                    com.plotsquared.core.location.Location pLoc = BukkitUtil.adapt(entity.getLocation());
877                                    PlotArea area = pLoc.getPlotArea();
878                                    if (area != null) {
879                                        Plot currentPlot = area.getPlotAbs(pLoc);
880                                        if (currentPlot != null) {
881                                            entity.setMetadata(
882                                                    "shulkerPlot",
883                                                    new FixedMetadataValue((Plugin) PlotSquared.platform(), currentPlot.getId())
884                                            );
885                                        }
886                                    }
887                                }
888                            }
889                            continue;
890                        case "ZOMBIFIED_PIGLIN":
891                        case "PIGLIN_BRUTE":
892                        case "LLAMA":
893                        case "DONKEY":
894                        case "MULE":
895                        case "ZOMBIE_HORSE":
896                        case "SKELETON_HORSE":
897                        case "HUSK":
898                        case "ELDER_GUARDIAN":
899                        case "WITHER_SKELETON":
900                        case "STRAY":
901                        case "ZOMBIE_VILLAGER":
902                        case "EVOKER":
903                        case "EVOKER_FANGS":
904                        case "VEX":
905                        case "VINDICATOR":
906                        case "POLAR_BEAR":
907                        case "BAT":
908                        case "BLAZE":
909                        case "CAVE_SPIDER":
910                        case "CHICKEN":
911                        case "COW":
912                        case "CREEPER":
913                        case "ENDERMAN":
914                        case "ENDERMITE":
915                        case "ENDER_DRAGON":
916                        case "GHAST":
917                        case "GIANT":
918                        case "GUARDIAN":
919                        case "HORSE":
920                        case "IRON_GOLEM":
921                        case "MAGMA_CUBE":
922                        case "MUSHROOM_COW":
923                        case "OCELOT":
924                        case "PIG":
925                        case "PIG_ZOMBIE":
926                        case "RABBIT":
927                        case "SHEEP":
928                        case "SILVERFISH":
929                        case "SKELETON":
930                        case "SLIME":
931                        case "SNOWMAN":
932                        case "SPIDER":
933                        case "SQUID":
934                        case "VILLAGER":
935                        case "WITCH":
936                        case "WITHER":
937                        case "WOLF":
938                        case "ZOMBIE":
939                        case "PARROT":
940                        case "SALMON":
941                        case "DOLPHIN":
942                        case "TROPICAL_FISH":
943                        case "DROWNED":
944                        case "COD":
945                        case "TURTLE":
946                        case "PUFFERFISH":
947                        case "PHANTOM":
948                        case "ILLUSIONER":
949                        case "CAT":
950                        case "PANDA":
951                        case "FOX":
952                        case "PILLAGER":
953                        case "TRADER_LLAMA":
954                        case "WANDERING_TRADER":
955                        case "RAVAGER":
956                        case "BEE":
957                        case "HOGLIN":
958                        case "PIGLIN":
959                        case "ZOGLIN":
960                        default: {
961                            if (Settings.Enabled_Components.KILL_ROAD_MOBS) {
962                                Location location = entity.getLocation();
963                                if (BukkitUtil.adapt(location).isPlotRoad()) {
964                                    if (entity instanceof LivingEntity livingEntity) {
965                                        if ((Settings.Enabled_Components.KILL_OWNED_ROAD_MOBS || !livingEntity.isLeashed())
966                                                || !entity.hasMetadata("keep")) {
967                                            Entity passenger = entity.getPassenger();
968                                            if ((Settings.Enabled_Components.KILL_OWNED_ROAD_MOBS
969                                                    || !((passenger instanceof Player) || livingEntity.isLeashed()))
970                                                    && (Settings.Enabled_Components.KILL_NAMED_ROAD_MOBS || entity.getCustomName() == null)
971                                                    && entity.getMetadata("keep").isEmpty()) {
972                                                if (entity.hasMetadata("ps-tmp-teleport")) {
973                                                    continue;
974                                                }
975                                                this.removeRoadEntity(entity, iterator);
976                                            }
977                                        }
978                                    } else {
979                                        Entity passenger = entity.getPassenger();
980                                        if ((Settings.Enabled_Components.KILL_OWNED_ROAD_MOBS || !(passenger instanceof Player))
981                                                && (Settings.Enabled_Components.KILL_NAMED_ROAD_MOBS && entity.getCustomName() != null)
982                                                && entity.getMetadata("keep").isEmpty()) {
983                                            if (entity.hasMetadata("ps-tmp-teleport")) {
984                                                continue;
985                                            }
986                                            this.removeRoadEntity(entity, iterator);
987                                        }
988                                    }
989                                }
990                            }
991                        }
992                    }
993                }
994            } catch (Throwable e) {
995                e.printStackTrace();
996            }
997        }), TaskTime.seconds(1L));
998    }
999
1000    private void removeRoadEntity(Entity entity, Iterator<Entity> entityIterator) {
1001        RemoveRoadEntityEvent event = eventDispatcher.callRemoveRoadEntity(BukkitAdapter.adapt(entity));
1002
1003        if (event.getEventResult() == Result.DENY) {
1004            return;
1005        }
1006
1007        entityIterator.remove();
1008        entity.remove();
1009    }
1010
1011    @Override
1012    public @Nullable
1013    final ChunkGenerator getDefaultWorldGenerator(
1014            final @NonNull String worldName,
1015            final @Nullable String id
1016    ) {
1017        final IndependentPlotGenerator result;
1018        if (id != null && id.equalsIgnoreCase("single")) {
1019            result = injector().getInstance(SingleWorldGenerator.class);
1020        } else {
1021            result = injector().getInstance(Key.get(IndependentPlotGenerator.class, DefaultGenerator.class));
1022            if (!PlotSquared.get().setupPlotWorld(worldName, id, result)) {
1023                return null;
1024            }
1025        }
1026        return (ChunkGenerator) result.specify(worldName);
1027    }
1028
1029    @Override
1030    public @Nullable GeneratorWrapper<?> getGenerator(
1031            final @NonNull String world,
1032            final @Nullable String name
1033    ) {
1034        if (name == null) {
1035            return null;
1036        }
1037        final Plugin genPlugin = Bukkit.getPluginManager().getPlugin(name);
1038        if (genPlugin != null && genPlugin.isEnabled()) {
1039            ChunkGenerator gen = genPlugin.getDefaultWorldGenerator(world, "");
1040            if (gen instanceof GeneratorWrapper<?>) {
1041                return (GeneratorWrapper<?>) gen;
1042            }
1043            return new BukkitPlotGenerator(world, gen, this.plotAreaManager);
1044        } else {
1045            return new BukkitPlotGenerator(
1046                    world,
1047                    injector().getInstance(Key.get(IndependentPlotGenerator.class, DefaultGenerator.class)),
1048                    this.plotAreaManager
1049            );
1050        }
1051    }
1052
1053    @Override
1054    public void startMetrics() {
1055        if (this.metricsStarted) {
1056            return;
1057        }
1058        this.metricsStarted = true;
1059        Metrics metrics = new Metrics(this, BSTATS_ID); // bstats
1060        metrics.addCustomChart(new DrilldownPie("area_types", () -> {
1061            final Map<String, Map<String, Integer>> map = new HashMap<>();
1062            for (final PlotAreaType plotAreaType : PlotAreaType.values()) {
1063                final Map<String, Integer> terrainTypes = new HashMap<>();
1064                for (final PlotAreaTerrainType plotAreaTerrainType : PlotAreaTerrainType.values()) {
1065                    terrainTypes.put(plotAreaTerrainType.name().toLowerCase(), 0);
1066                }
1067                map.put(plotAreaType.name().toLowerCase(), terrainTypes);
1068            }
1069            for (final PlotArea plotArea : this.plotAreaManager.getAllPlotAreas()) {
1070                final Map<String, Integer> terrainTypeMap = map.get(plotArea.getType().name().toLowerCase());
1071                terrainTypeMap.put(
1072                        plotArea.getTerrain().name().toLowerCase(),
1073                        terrainTypeMap.get(plotArea.getTerrain().name().toLowerCase()) + 1
1074                );
1075            }
1076            return map;
1077        }));
1078        metrics.addCustomChart(new SimplePie(
1079                "premium",
1080                () -> PremiumVerification.isPremium() ? "Premium" : "Non-Premium"
1081        ));
1082        metrics.addCustomChart(new SimplePie("worlds", () -> Settings.Enabled_Components.WORLDS ? "true" : "false"));
1083        metrics.addCustomChart(new SimplePie("economy", () -> Settings.Enabled_Components.ECONOMY ? "true" : "false"));
1084        metrics.addCustomChart(new SimplePie(
1085                "plot_expiry",
1086                () -> Settings.Enabled_Components.PLOT_EXPIRY ? "true" : "false"
1087        ));
1088        metrics.addCustomChart(new SimplePie("database_type", () -> Storage.MySQL.USE ? "MySQL" : "SQLite"));
1089        metrics.addCustomChart(new SimplePie(
1090                "worldedit_implementation",
1091                () -> Bukkit.getPluginManager().getPlugin("FastAsyncWorldEdit") != null ? "FastAsyncWorldEdit" : "WorldEdit"
1092        ));
1093        metrics.addCustomChart(new SimplePie("offline_mode", () -> Settings.UUID.OFFLINE ? "true" : "false"));
1094        metrics.addCustomChart(new SimplePie("offline_mode_force", () -> Settings.UUID.FORCE_LOWERCASE ? "true" : "false"));
1095    }
1096
1097    @Override
1098    public void unregister(final @NonNull PlotPlayer<?> player) {
1099        PlotSquared.platform().playerManager().removePlayer(player.getUUID());
1100    }
1101
1102    @Override
1103    public void setGenerator(final @NonNull String worldName) {
1104        World world = BukkitUtil.getWorld(worldName);
1105        if (world == null) {
1106            // create world
1107            ConfigurationSection worldConfig = this.worldConfiguration.getConfigurationSection("worlds." + worldName);
1108            String manager = worldConfig.getString("generator.plugin", pluginName());
1109            PlotAreaBuilder builder =
1110                    PlotAreaBuilder.newBuilder().plotManager(manager).generatorName(worldConfig.getString(
1111                                    "generator.init",
1112                                    manager
1113                            ))
1114                            .plotAreaType(ConfigurationUtil.getType(worldConfig)).terrainType(ConfigurationUtil.getTerrain(
1115                                    worldConfig))
1116                            .settingsNodesWrapper(new SettingsNodesWrapper(new ConfigurationNode[0], null)).worldName(worldName);
1117            injector().getInstance(SetupUtils.class).setupWorld(builder);
1118            world = Bukkit.getWorld(worldName);
1119        } else {
1120            try {
1121                if (!this.plotAreaManager.hasPlotArea(worldName)) {
1122                    SetGenCB.setGenerator(BukkitUtil.getWorld(worldName));
1123                }
1124            } catch (final Exception e) {
1125                LOGGER.error("Failed to reload world: {} | {}", world, e.getMessage());
1126                Bukkit.getServer().unloadWorld(world, false);
1127                return;
1128            }
1129        }
1130        assert world != null;
1131        ChunkGenerator gen = world.getGenerator();
1132        if (gen instanceof BukkitPlotGenerator) {
1133            PlotSquared.get().loadWorld(worldName, (BukkitPlotGenerator) gen);
1134        } else if (gen != null) {
1135            PlotSquared.get().loadWorld(worldName, new BukkitPlotGenerator(worldName, gen, this.plotAreaManager));
1136        } else if (this.worldConfiguration.contains("worlds." + worldName)) {
1137            PlotSquared.get().loadWorld(worldName, null);
1138        }
1139    }
1140
1141    @Override
1142    public @NonNull String serverNativePackage() {
1143        final String name = Bukkit.getServer().getClass().getPackage().getName();
1144        return name.substring(name.lastIndexOf('.') + 1);
1145    }
1146
1147    @Override
1148    public @NonNull GeneratorWrapper<?> wrapPlotGenerator(
1149            final @NonNull String world,
1150            final @NonNull IndependentPlotGenerator generator
1151    ) {
1152        return new BukkitPlotGenerator(world, generator, this.plotAreaManager);
1153    }
1154
1155    @Override
1156    public @NonNull String pluginsFormatted() {
1157        StringBuilder msg = new StringBuilder();
1158        List<Plugin> plugins = new ArrayList<>();
1159        Collections.addAll(plugins, Bukkit.getServer().getPluginManager().getPlugins());
1160        plugins.sort(Comparator.comparing(Plugin::getName));
1161        msg.append("Plugins (").append(plugins.size()).append("): \n");
1162        for (Plugin p : plugins) {
1163            msg.append(" - ").append(p.getName()).append(":").append("\n")
1164                    .append("  • Version: ").append(p.getDescription().getVersion()).append("\n")
1165                    .append("  • Enabled: ").append(p.isEnabled()).append("\n")
1166                    .append("  • Main: ").append(p.getDescription().getMain()).append("\n")
1167                    .append("  • Authors: ").append(p.getDescription().getAuthors()).append("\n")
1168                    .append("  • Load Before: ").append(p.getDescription().getLoadBefore()).append("\n")
1169                    .append("  • Dependencies: ").append(p.getDescription().getDepend()).append("\n")
1170                    .append("  • Soft Dependencies: ").append(p.getDescription().getSoftDepend()).append("\n");
1171        }
1172        return msg.toString();
1173    }
1174
1175    @Override
1176    @SuppressWarnings("ConstantConditions")
1177    public @NonNull String worldEditImplementations() {
1178        StringBuilder msg = new StringBuilder();
1179        if (Bukkit.getPluginManager().getPlugin("FastAsyncWorldEdit") != null) {
1180            msg.append("FastAsyncWorldEdit: ").append(Bukkit.getPluginManager().getPlugin("FastAsyncWorldEdit").getDescription().getVersion());
1181        } else if (Bukkit.getPluginManager().getPlugin("AsyncWorldEdit") != null) {
1182            msg.append("AsyncWorldEdit: ").append(Bukkit.getPluginManager().getPlugin("AsyncWorldEdit").getDescription().getVersion()).append("\n");
1183            msg.append("WorldEdit: ").append(Bukkit.getPluginManager().getPlugin("WorldEdit").getDescription().getVersion());
1184        } else {
1185            msg.append("WorldEdit: ").append(Bukkit.getPluginManager().getPlugin("WorldEdit").getDescription().getVersion());
1186        }
1187        return msg.toString();
1188    }
1189
1190    @Override
1191    public com.plotsquared.core.location.@NonNull World<?> getPlatformWorld(final @NonNull String worldName) {
1192        return BukkitWorld.of(worldName);
1193    }
1194
1195    @Override
1196    public @NonNull Audience consoleAudience() {
1197        return BukkitUtil.BUKKIT_AUDIENCES.console();
1198    }
1199
1200    @Override
1201    public @NonNull String pluginName() {
1202        return this.pluginName;
1203    }
1204
1205    public SingleWorldListener getSingleWorldListener() {
1206        return this.singleWorldListener;
1207    }
1208
1209    @Override
1210    public @NonNull Injector injector() {
1211        return this.injector;
1212    }
1213
1214    @Override
1215    public @NonNull PlotAreaManager plotAreaManager() {
1216        return this.plotAreaManager;
1217    }
1218
1219    @NonNull
1220    @Override
1221    public Locale getLocale() {
1222        return this.serverLocale;
1223    }
1224
1225    @Override
1226    public void setLocale(final @NonNull Locale locale) {
1227        throw new UnsupportedOperationException("Cannot replace server locale");
1228    }
1229
1230    @Override
1231    public @NonNull PlatformWorldManager<?> worldManager() {
1232        return injector().getInstance(Key.get(new TypeLiteral<PlatformWorldManager<World>>() {
1233        }));
1234    }
1235
1236    @Override
1237    @NonNull
1238    @SuppressWarnings("unchecked")
1239    public PlayerManager<? extends PlotPlayer<Player>, ? extends Player> playerManager() {
1240        return (PlayerManager<BukkitPlayer, Player>) injector().getInstance(PlayerManager.class);
1241    }
1242
1243    @Override
1244    public void copyCaptionMaps() {
1245        /* Make this prettier at some point */
1246        final String[] languages = new String[]{"en"};
1247        for (final String language : languages) {
1248            if (!new File(new File(this.getDataFolder(), "lang"), String.format("messages_%s.json", language)).exists()) {
1249                this.saveResource(String.format("lang/messages_%s.json", language), false);
1250                LOGGER.info("Copied language file 'messages_{}.json'", language);
1251            }
1252        }
1253    }
1254
1255    @NonNull
1256    @Override
1257    public String toLegacyPlatformString(final @NonNull Component component) {
1258        return LegacyComponentSerializer.legacyAmpersand().serialize(component);
1259    }
1260
1261    @Override
1262    public boolean isFaweHooking() {
1263        return faweHook;
1264    }
1265
1266}