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