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