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