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