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