PageRenderTime 77ms CodeModel.GetById 32ms app.highlight 35ms RepoModel.GetById 1ms app.codeStats 0ms

/src/main/java/net/glowstone/GlowServer.java

https://gitlab.com/StoneGroup/StoneCore
Java | 1320 lines | 819 code | 129 blank | 372 comment | 123 complexity | 61a80b4ea90235384b5c238aca6ff206 MD5 | raw file
   1package net.glowstone;
   2
   3import com.flowpowered.network.Message;
   4import com.google.common.base.Preconditions;
   5import com.google.common.collect.Iterators;
   6import com.jogamp.opencl.CLDevice;
   7import com.jogamp.opencl.CLPlatform;
   8import io.netty.channel.epoll.Epoll;
   9import lombok.Getter;
  10import net.glowstone.advancement.GlowAdvancement;
  11import net.glowstone.advancement.GlowAdvancementDisplay;
  12import net.glowstone.block.BuiltinMaterialValueManager;
  13import net.glowstone.block.MaterialValueManager;
  14import net.glowstone.block.entity.state.GlowDispenser;
  15import net.glowstone.boss.BossBarManager;
  16import net.glowstone.boss.GlowBossBar;
  17import net.glowstone.client.GlowClient;
  18import net.glowstone.command.glowstone.ColorCommand;
  19import net.glowstone.command.glowstone.GlowstoneCommand;
  20import net.glowstone.command.minecraft.*;
  21import net.glowstone.constants.GlowEnchantment;
  22import net.glowstone.constants.GlowPotionEffect;
  23import net.glowstone.entity.EntityIdManager;
  24import net.glowstone.entity.GlowPlayer;
  25import net.glowstone.entity.meta.profile.PlayerProfile;
  26import net.glowstone.generator.*;
  27import net.glowstone.inventory.GlowInventory;
  28import net.glowstone.inventory.GlowItemFactory;
  29import net.glowstone.inventory.crafting.CraftingManager;
  30import net.glowstone.io.PlayerDataService;
  31import net.glowstone.io.PlayerStatisticIoService;
  32import net.glowstone.io.ScoreboardIoService;
  33import net.glowstone.map.GlowMapView;
  34import net.glowstone.net.GameServer;
  35import net.glowstone.net.SessionRegistry;
  36import net.glowstone.net.message.play.player.AdvancementsMessage;
  37import net.glowstone.net.query.QueryServer;
  38import net.glowstone.net.rcon.RconServer;
  39import net.glowstone.scheduler.GlowScheduler;
  40import net.glowstone.scheduler.WorldScheduler;
  41import net.glowstone.scoreboard.GlowScoreboardManager;
  42import net.glowstone.util.*;
  43import net.glowstone.util.bans.GlowBanList;
  44import net.glowstone.util.bans.UuidListFile;
  45import net.glowstone.util.config.ServerConfig;
  46import net.glowstone.util.config.ServerConfig.Key;
  47import net.glowstone.util.config.WorldConfig;
  48import net.glowstone.util.loot.LootingManager;
  49import net.md_5.bungee.api.chat.BaseComponent;
  50import org.bukkit.*;
  51import org.bukkit.BanList.Type;
  52import org.bukkit.Warning.WarningState;
  53import org.bukkit.World.Environment;
  54import org.bukkit.advancement.Advancement;
  55import org.bukkit.boss.BarColor;
  56import org.bukkit.boss.BarFlag;
  57import org.bukkit.boss.BarStyle;
  58import org.bukkit.boss.BossBar;
  59import org.bukkit.command.*;
  60import org.bukkit.configuration.ConfigurationSection;
  61import org.bukkit.configuration.MemorySection;
  62import org.bukkit.configuration.serialization.ConfigurationSerialization;
  63import org.bukkit.entity.Entity;
  64import org.bukkit.entity.Player;
  65import org.bukkit.event.inventory.InventoryType;
  66import org.bukkit.generator.ChunkGenerator;
  67import org.bukkit.generator.ChunkGenerator.ChunkData;
  68import org.bukkit.help.HelpMap;
  69import org.bukkit.inventory.*;
  70import org.bukkit.permissions.Permissible;
  71import org.bukkit.permissions.Permission;
  72import org.bukkit.permissions.PermissionDefault;
  73import org.bukkit.plugin.*;
  74import org.bukkit.plugin.java.JavaPluginLoader;
  75import org.bukkit.plugin.messaging.Messenger;
  76import org.bukkit.plugin.messaging.StandardMessenger;
  77import org.bukkit.util.CachedServerIcon;
  78import org.bukkit.util.permissions.DefaultPermissions;
  79
  80import java.awt.image.BufferedImage;
  81import java.io.File;
  82import java.io.IOException;
  83import java.net.InetSocketAddress;
  84import java.nio.file.*;
  85import java.nio.file.attribute.BasicFileAttributes;
  86import java.security.KeyPair;
  87import java.util.*;
  88import java.util.concurrent.CountDownLatch;
  89import java.util.logging.Level;
  90import java.util.logging.Logger;
  91import java.util.stream.Collectors;
  92
  93/**
  94 * The core class of the Glowstone server.
  95 *
  96 * @author Graham Edgecombe
  97 */
  98public final class GlowServer implements Server {
  99
 100    /**
 101     * The logger for this class.
 102     */
 103    public static final Logger logger = Logger.getLogger("Minecraft");
 104    /**
 105     * The game version supported by the server.
 106     */
 107    public static final String GAME_VERSION = "1.12.2";
 108    /**
 109     * The protocol version supported by the server.
 110     */
 111    public static final int PROTOCOL_VERSION = 340;
 112    /**
 113     * A list of all the active {@link net.glowstone.net.GlowSession}s.
 114     */
 115    private final SessionRegistry sessions = new SessionRegistry();
 116    /**
 117     * The console manager of this server.
 118     */
 119    private final ConsoleManager consoleManager = new ConsoleManager(this);
 120    /**
 121     * The services manager of this server.
 122     */
 123    private final SimpleServicesManager servicesManager = new SimpleServicesManager();
 124    /**
 125     * The command map of this server.
 126     */
 127    private final SimpleCommandMap commandMap = new SimpleCommandMap(this);
 128    /**
 129     * The plugin manager of this server.
 130     */
 131    private final SimplePluginManager pluginManager = new SimplePluginManager(this, commandMap);
 132    /**
 133     * The plugin channel messenger for the server.
 134     */
 135    private final Messenger messenger = new StandardMessenger();
 136    /**
 137     * The help map for the server.
 138     */
 139    private final GlowHelpMap helpMap = new GlowHelpMap(this);
 140    /**
 141     * The scoreboard manager for the server.
 142     */
 143    private final GlowScoreboardManager scoreboardManager = new GlowScoreboardManager(this);
 144    /**
 145     * The crafting manager for this server.
 146     */
 147    private final CraftingManager craftingManager = new CraftingManager();
 148    /**
 149     * The configuration for the server.
 150     */
 151    private final ServerConfig config;
 152    /**
 153     * The world config for extended world customization.
 154     */
 155    private static WorldConfig worldConfig;
 156    /**
 157     * The list of OPs on the server.
 158     */
 159    private final UuidListFile opsList;
 160    /**
 161     * The list of players whitelisted on the server.
 162     */
 163    private final UuidListFile whitelist;
 164    /**
 165     * The BanList for player names.
 166     */
 167    private final GlowBanList nameBans;
 168    /**
 169     * The BanList for IP addresses.
 170     */
 171    private final GlowBanList ipBans;
 172    /**
 173     * The EntityIdManager for this server.
 174     */
 175    private final EntityIdManager entityIdManager = new EntityIdManager();
 176    /**
 177     * The world this server is managing.
 178     */
 179    private final WorldScheduler worlds = new WorldScheduler();
 180    /**
 181     * The task scheduler used by this server.
 182     */
 183    private final GlowScheduler scheduler = new GlowScheduler(this, worlds);
 184    /**
 185     * The Bukkit UnsafeValues implementation.
 186     */
 187    private final UnsafeValues unsafeAccess = new GlowUnsafeValues();
 188    /**
 189     * A RSA key pair used for encryption and authentication
 190     */
 191    private final KeyPair keyPair = SecurityUtils.generateKeyPair();
 192    /**
 193     * The network server used for network communication
 194     */
 195    @Getter
 196    private GameServer networkServer;
 197    /**
 198     * A set of all online players.
 199     */
 200    private final Set<GlowPlayer> onlinePlayers = new HashSet<>();
 201    /**
 202     * A view of all online players.
 203     */
 204    private final Set<GlowPlayer> onlineView = Collections.unmodifiableSet(onlinePlayers);
 205    /**
 206     * The plugin type detector of thi server.
 207     */
 208    private GlowPluginTypeDetector pluginTypeDetector;
 209    /**
 210     * The server's default game mode
 211     */
 212    private GameMode defaultGameMode = GameMode.CREATIVE;
 213
 214    /**
 215     * The setting for verbose deprecation warnings.
 216     */
 217    private WarningState warnState = WarningState.DEFAULT;
 218
 219    /**
 220     * Whether the server is shutting down
 221     */
 222    private boolean isShuttingDown;
 223
 224    /**
 225     * Whether the whitelist is in effect.
 226     */
 227    private boolean whitelistEnabled;
 228
 229    /**
 230     * The size of the area to keep protected around the spawn point.
 231     */
 232    private int spawnRadius;
 233
 234    /**
 235     * The ticks until a player who has not played the game has been kicked, or 0.
 236     */
 237    private int idleTimeout;
 238    /**
 239     * The query server for this server, or null if disabled.
 240     */
 241    private QueryServer queryServer;
 242    /**
 243     * The Rcon server for this server, or null if disabled.
 244     */
 245    private RconServer rconServer;
 246    /**
 247     * The default icon, usually blank, used for the server list.
 248     */
 249    private GlowServerIcon defaultIcon;
 250    /**
 251     * The server port.
 252     */
 253    private int port;
 254    /**
 255     * The server ip.
 256     */
 257    private String ip;
 258    /**
 259     * The {@link MaterialValueManager} of this server.
 260     */
 261    private MaterialValueManager materialValueManager;
 262    /**
 263     * The {@link BossBarManager} of this server.
 264     */
 265    private BossBarManager bossBarManager;
 266    /**
 267     * Default root permissions
 268     */
 269    public Permission permissionRoot, permissionCommand;
 270    /**
 271     * The {@link GlowAdvancement}s of this server.
 272     */
 273    private final Map<NamespacedKey, Advancement> advancements;
 274
 275    /**
 276     * Creates a new server.
 277     *
 278     * @param config This server's config.
 279     */
 280    public GlowServer(ServerConfig config) {
 281        materialValueManager = new BuiltinMaterialValueManager();
 282        bossBarManager = new BossBarManager(this);
 283        advancements = new HashMap<>();
 284        // test advancement
 285        GlowAdvancement advancement = new GlowAdvancement(NamespacedKey.minecraft("test"), null);
 286        advancement.addCriterion("minecraft:test/criterion");
 287        advancement.setDisplay(new GlowAdvancementDisplay(new TextMessage("Advancements in Glowstone"),
 288                new TextMessage("=)"), new ItemStack(Material.GLOWSTONE), GlowAdvancementDisplay.FrameType.GOAL, -10F, 0));
 289        addAdvancement(advancement);
 290
 291        this.config = config;
 292        // stuff based on selected config directory
 293        opsList = new UuidListFile(config.getFile("ops.json"));
 294        whitelist = new UuidListFile(config.getFile("whitelist.json"));
 295        nameBans = new GlowBanList(this, Type.NAME);
 296        ipBans = new GlowBanList(this, Type.IP);
 297
 298        Bukkit.setServer(this);
 299        loadConfig();
 300    }
 301
 302    /**
 303     * The client instance backed by this server.
 304     */
 305    public static GlowClient client;
 306
 307    /**
 308     * Creates a new server on TCP port 25565 and starts listening for
 309     * connections.
 310     *
 311     * @param args The command-line arguments.
 312     */
 313    public static void main(String... args) {
 314        try {
 315            GlowServer server = createFromArguments(args);
 316
 317            // we don't want to run a server when called with --version
 318            if (server == null) {
 319                return;
 320            }
 321
 322            server.run();
 323        } catch (SecurityException e) {
 324            logger.log(Level.WARNING, "Error loading classpath!", e);
 325        } catch (Throwable t) {
 326            // general server startup crash
 327            logger.log(Level.SEVERE, "Error during server startup.", t);
 328            System.exit(1);
 329        }
 330    }
 331
 332    private static GlowServer createFromArguments(String... args) {
 333        ServerConfig config = parseArguments(args);
 334
 335        // we don't want to create a server when called with --version
 336        if (config == null) {
 337            return null;
 338        }
 339
 340        ConfigurationSerialization.registerClass(GlowOfflinePlayer.class);
 341        GlowPotionEffect.register();
 342        GlowEnchantment.register();
 343        GlowDispenser.register();
 344
 345        return new GlowServer(config);
 346    }
 347
 348    private static ServerConfig parseArguments(String... args) {
 349        Map<Key, Object> parameters = new EnumMap<>(Key.class);
 350        String configDirName = "config";
 351        String configFileName = "glowstone.yml";
 352
 353        // Calculate acceptable parameters
 354        for (int i = 0; i < args.length; i++) {
 355            String opt = args[i];
 356
 357            if (opt.isEmpty() || opt.charAt(0) != '-') {
 358                System.err.println("Ignored invalid option: " + opt);
 359                continue;
 360            }
 361
 362            // Help and version
 363            if ("--help".equals(opt) || "-h".equals(opt) || "-?".equals(opt)) {
 364                System.out.println("Available command-line options:");
 365                System.out.println("  --help, -h, -?                 Shows this help message and exits.");
 366                System.out.println("  --version, -v                  Shows version information and exits.");
 367                System.out.println("  --configdir <directory>        Sets the configuration directory.");
 368                System.out.println("  --configfile <file>            Sets the configuration file.");
 369                System.out.println("  --port, -p <port>              Sets the server listening port.");
 370                System.out.println("  --host, -H <ip | hostname>     Sets the server listening address.");
 371                System.out.println("  --onlinemode, -o <onlinemode>  Sets the server's online-mode.");
 372                System.out.println("  --jline <true/false>           Enables or disables JLine console.");
 373                System.out.println("  --plugins-dir, -P <directory>  Sets the plugin directory to use.");
 374                System.out.println("  --worlds-dir, -W <directory>   Sets the world directory to use.");
 375                System.out.println("  --update-dir, -U <directory>   Sets the plugin update folder to use.");
 376                System.out.println("  --max-players, -M <director>   Sets the maximum amount of players.");
 377                System.out.println("  --world-name, -N <name>        Sets the main world name.");
 378                System.out.println("  --log-pattern, -L <pattern>    Sets the log file pattern (%D for date).");
 379                return null;
 380            } else if ("--version".equals(opt) || "-v".equals(opt)) {
 381                System.out.println("Glowstone version: " + GlowServer.class.getPackage().getImplementationVersion());
 382                System.out.println("Bukkit version:    " + GlowServer.class.getPackage().getSpecificationVersion());
 383                System.out.println("Minecraft version: " + GAME_VERSION + " protocol " + PROTOCOL_VERSION);
 384                return null;
 385            }
 386
 387            // Below this point, options require parameters
 388            if (i == args.length - 1) {
 389                System.err.println("Ignored option specified without value: " + opt);
 390                continue;
 391            }
 392
 393            switch (opt) {
 394                case "--configdir":
 395                    configDirName = args[++i];
 396                    break;
 397                case "--configfile":
 398                    configFileName = args[++i];
 399                    break;
 400                case "--port":
 401                case "-p":
 402                    parameters.put(Key.SERVER_PORT, Integer.valueOf(args[++i]));
 403                    break;
 404                case "--host":
 405                case "-H":
 406                    parameters.put(Key.SERVER_IP, args[++i]);
 407                    break;
 408                case "--onlinemode":
 409                case "-o":
 410                    parameters.put(Key.ONLINE_MODE, Boolean.valueOf(args[++i]));
 411                    break;
 412                case "--jline":
 413                    parameters.put(Key.USE_JLINE, Boolean.valueOf(args[++i]));
 414                    break;
 415                case "--plugins-dir":
 416                case "-P":
 417                    parameters.put(Key.PLUGIN_FOLDER, args[++i]);
 418                    break;
 419                case "--worlds-dir":
 420                case "-W":
 421                    parameters.put(Key.WORLD_FOLDER, args[++i]);
 422                    break;
 423                case "--update-dir":
 424                case "-U":
 425                    parameters.put(Key.UPDATE_FOLDER, args[++i]);
 426                    break;
 427                case "--max-players":
 428                case "-M":
 429                    parameters.put(Key.MAX_PLAYERS, Integer.valueOf(args[++i]));
 430                    break;
 431                case "--world-name":
 432                case "-N":
 433                    parameters.put(Key.LEVEL_NAME, args[++i]);
 434                    break;
 435                case "--log-pattern":
 436                case "-L":
 437                    parameters.put(Key.LOG_FILE, args[++i]);
 438                    break;
 439                default:
 440                    System.err.println("Ignored invalid option: " + opt);
 441            }
 442        }
 443
 444        File configDir = new File(configDirName);
 445        worldConfig = new WorldConfig(configDir, new File(configDir, "worlds.yml"));
 446        File configFile = new File(configDir, configFileName);
 447
 448        return new ServerConfig(configDir, configFile, parameters);
 449    }
 450
 451    /**
 452     * Starts the server starting sequence (starting, binding to port, etc.)
 453     */
 454    public void run() {
 455        start();
 456        bind();
 457        logger.info("Ready for connections.");
 458
 459        if (doMetrics()) {
 460            new Metrics(this, config.getString(Key.METRICS_UUID), false);
 461        }
 462
 463        if (config.getBoolean(Key.RUN_CLIENT)) {
 464            client = new GlowClient(this);
 465            client.run();
 466        }
 467    }
 468
 469    /**
 470     * Whether OpenCL is to be used by the server on this run.
 471     */
 472    private boolean isCLApplicable = true;
 473
 474    /**
 475     * Starts this server.
 476     */
 477    public void start() {
 478        // Determine console mode and start reading input
 479        consoleManager.startConsole(config.getBoolean(Key.USE_JLINE));
 480        consoleManager.startFile(config.getString(Key.LOG_FILE));
 481
 482        if (getProxySupport()) {
 483            if (getOnlineMode()) {
 484                logger.warning("Proxy support is enabled, but online mode is enabled.");
 485            } else {
 486                logger.info("Proxy support is enabled.");
 487            }
 488        } else if (!getOnlineMode()) {
 489            logger.warning("The server is running in offline mode! Only do this if you know what you're doing.");
 490        }
 491
 492        int openCLMajor = 1;
 493        int openCLMinor = 2;
 494
 495        if (doesUseGPGPU()) {
 496            int maxGpuFlops = 0;
 497            int maxIntelFlops = 0;
 498            int maxCpuFlops = 0;
 499            CLPlatform bestPlatform = null;
 500            CLPlatform bestIntelPlatform = null;
 501            CLPlatform bestCpuPlatform = null;
 502            // gets the max flops device across platforms on the computer
 503            for (CLPlatform platform : CLPlatform.listCLPlatforms()) {
 504                if (platform.isAtLeast(openCLMajor, openCLMinor) && platform.isExtensionAvailable("cl_khr_fp64")) {
 505                    for (CLDevice device : platform.listCLDevices()) {
 506                        if (device.getType() == CLDevice.Type.GPU) {
 507                            int flops = device.getMaxComputeUnits() * device.getMaxClockFrequency();
 508                            logger.info("Found " + device + " with " + flops + " flops");
 509                            if (device.getVendor().contains("Intel")) {
 510                                if (flops > maxIntelFlops) {
 511                                    maxIntelFlops = flops;
 512                                    logger.info("Device is best platform so far, on " + platform);
 513                                    bestIntelPlatform = platform;
 514                                } else if (flops == maxIntelFlops) {
 515                                    if (bestIntelPlatform != null && bestIntelPlatform.getVersion().compareTo(platform.getVersion()) < 0) {
 516                                        maxIntelFlops = flops;
 517                                        logger.info("Device tied for flops, but had higher version on " + platform);
 518                                        bestIntelPlatform = platform;
 519                                    }
 520                                }
 521                            } else {
 522                                if (flops > maxGpuFlops) {
 523                                    maxGpuFlops = flops;
 524                                    logger.info("Device is best platform so far, on " + platform);
 525                                    bestPlatform = platform;
 526                                } else if (flops == maxGpuFlops) {
 527                                    if (bestPlatform != null && bestPlatform.getVersion().compareTo(platform.getVersion()) < 0) {
 528                                        maxGpuFlops = flops;
 529                                        logger.info("Device tied for flops, but had higher version on " + platform);
 530                                        bestPlatform = platform;
 531                                    }
 532                                }
 533                            }
 534                        } else {
 535                            int flops = device.getMaxComputeUnits() * device.getMaxClockFrequency();
 536                            logger.info("Found " + device + " with " + flops + " flops");
 537                            if (flops > maxCpuFlops) {
 538                                maxCpuFlops = flops;
 539                                logger.info("Device is best platform so far, on " + platform);
 540                                bestCpuPlatform = platform;
 541                            } else if (flops == maxCpuFlops) {
 542                                if (bestCpuPlatform != null && bestCpuPlatform.getVersion().compareTo(platform.getVersion()) < 0) {
 543                                    maxCpuFlops = flops;
 544                                    logger.info("Device tied for flops, but had higher version on " + platform);
 545                                    bestCpuPlatform = platform;
 546                                }
 547                            }
 548                        }
 549                    }
 550                }
 551            }
 552
 553            if (config.getBoolean(Key.GPGPU_ANY_DEVICE)) {
 554                if (maxGpuFlops - maxIntelFlops < 0 && maxCpuFlops - maxIntelFlops <= 0) {
 555                    bestPlatform = bestIntelPlatform;
 556                } else if (maxGpuFlops - maxCpuFlops < 0 && maxIntelFlops - maxCpuFlops < 0) {
 557                    bestPlatform = bestCpuPlatform;
 558                }
 559            } else {
 560                if (maxGpuFlops == 0) {
 561                    if (maxIntelFlops == 0) {
 562                        logger.info("No Intel graphics found, best platform is the best CPU platform we could find...");
 563                        bestPlatform = bestCpuPlatform;
 564                    } else {
 565                        logger.info("No dGPU found, best platform is the best Intel graphics we could find...");
 566                        bestPlatform = bestIntelPlatform;
 567                    }
 568                }
 569            }
 570
 571            if (bestPlatform == null) {
 572                isCLApplicable = false;
 573                logger.info("Your system does not meet the OpenCL requirements for Glowstone. See if driver updates are available.");
 574                logger.info("Required version: " + openCLMajor + '.' + openCLMinor);
 575                logger.info("Required extensions: [ cl_khr_fp64 ]");
 576            } else {
 577                OpenCL.initContext(bestPlatform);
 578            }
 579        }
 580
 581        // Load player lists
 582        opsList.load();
 583        whitelist.load();
 584        nameBans.load();
 585        ipBans.load();
 586        setPort(config.getInt(Key.SERVER_PORT));
 587        setIp(config.getString(Key.SERVER_IP));
 588
 589        try {
 590            LootingManager.load();
 591        } catch (Exception e) {
 592            GlowServer.logger.severe("Failed to load looting manager: ");
 593            e.printStackTrace();
 594        }
 595
 596        // Start loading plugins
 597        new LibraryManager().run();
 598        loadPlugins();
 599        enablePlugins(PluginLoadOrder.STARTUP);
 600
 601        // Create worlds
 602        String name = config.getString(Key.LEVEL_NAME);
 603        String seedString = config.getString(Key.LEVEL_SEED);
 604        boolean structs = getGenerateStructures();
 605        WorldType type = WorldType.getByName(getWorldType());
 606        if (type == null) {
 607            type = WorldType.NORMAL;
 608        }
 609
 610        long seed = new Random().nextLong();
 611        if (!seedString.isEmpty()) {
 612            try {
 613                long parsed = Long.parseLong(seedString);
 614                if (parsed != 0) {
 615                    seed = parsed;
 616                }
 617            } catch (NumberFormatException ex) {
 618                seed = seedString.hashCode();
 619            }
 620        }
 621
 622        createWorld(WorldCreator.name(name).environment(Environment.NORMAL).seed(seed).type(type).generateStructures(structs));
 623        if (getAllowNether()) {
 624            checkTransfer(name, "_nether", Environment.NETHER);
 625            createWorld(WorldCreator.name(name + "_nether").environment(Environment.NETHER).seed(seed).type(type).generateStructures(structs));
 626        }
 627        if (getAllowEnd()) {
 628            checkTransfer(name, "_the_end", Environment.THE_END);
 629            createWorld(WorldCreator.name(name + "_the_end").environment(Environment.THE_END).seed(seed).type(type).generateStructures(structs));
 630        }
 631
 632        // Finish loading plugins
 633        enablePlugins(PluginLoadOrder.POSTWORLD);
 634        commandMap.registerServerAliases();
 635        scheduler.start();
 636    }
 637
 638    private void checkTransfer(String name, String suffix, Environment environment) {
 639        // todo: import things like per-dimension villages.dat when those are implemented
 640        Path srcPath = new File(new File(getWorldContainer(), name), "DIM" + environment.getId()).toPath();
 641        Path destPath = new File(getWorldContainer(), name + suffix).toPath();
 642        if (Files.exists(srcPath) && !Files.exists(destPath)) {
 643            logger.info("Importing " + destPath + " from " + srcPath);
 644            try {
 645                Files.walkFileTree(srcPath, new FileVisitor<Path>() {
 646                    @Override
 647                    public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
 648                        Path target = destPath.resolve(srcPath.relativize(dir));
 649                        if (!Files.exists(target)) {
 650                            Files.createDirectory(target);
 651                        }
 652                        return FileVisitResult.CONTINUE;
 653                    }
 654
 655                    @Override
 656                    public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
 657                        Files.copy(file, destPath.resolve(srcPath.relativize(file)), StandardCopyOption.COPY_ATTRIBUTES);
 658                        return FileVisitResult.CONTINUE;
 659                    }
 660
 661                    @Override
 662                    public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
 663                        logger.warning("Importing file " + srcPath.relativize(file) + " + failed: " + exc);
 664                        return FileVisitResult.CONTINUE;
 665                    }
 666
 667                    @Override
 668                    public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
 669                        return FileVisitResult.CONTINUE;
 670                    }
 671                });
 672                Files.copy(srcPath.resolve("../level.dat"), destPath.resolve("level.dat"));
 673            } catch (IOException e) {
 674                logger.log(Level.WARNING, "Import of " + srcPath + " failed", e);
 675            }
 676        }
 677    }
 678
 679    private void bind() {
 680        if (Epoll.isAvailable()) {
 681            logger.info("Native epoll transport is enabled.");
 682        }
 683
 684        CountDownLatch latch = new CountDownLatch(3);
 685
 686        networkServer = new GameServer(this, latch);
 687        networkServer.bind(getBindAddress(Key.SERVER_PORT));
 688
 689        if (config.getBoolean(Key.QUERY_ENABLED)) {
 690            queryServer = new QueryServer(this, latch, config.getBoolean(Key.QUERY_PLUGINS));
 691            queryServer.bind(getBindAddress(Key.QUERY_PORT));
 692        } else {
 693            latch.countDown();
 694        }
 695
 696        if (config.getBoolean(Key.RCON_ENABLED)) {
 697            rconServer = new RconServer(this, latch, config.getString(Key.RCON_PASSWORD));
 698            rconServer.bind(getBindAddress(Key.RCON_PORT));
 699        } else {
 700            latch.countDown();
 701        }
 702
 703        try {
 704            latch.await();
 705        } catch (InterruptedException e) {
 706            logger.log(Level.SEVERE, "Bind interrupted! ", e);
 707            System.exit(1);
 708        }
 709    }
 710
 711    /**
 712     * Sets the port that the Query server will expose.
 713     *
 714     * <p>This does not change the port the server will run on.
 715     *
 716     * @param port the port number
 717     */
 718    public void setPort(int port) {
 719        this.port = port;
 720    }
 721
 722    /**
 723     * Sets the IP address that the Query server will expose.
 724     *
 725     * <p>This does not change the IP address the server will run on.
 726     *
 727     * @param ip the IP address
 728     */
 729    public void setIp(String ip) {
 730        this.ip = ip;
 731    }
 732
 733    /**
 734     * Get the SocketAddress to bind to for a specified service.
 735     *
 736     * @param portKey The configuration key for the port to use.
 737     * @return The SocketAddress
 738     */
 739    private InetSocketAddress getBindAddress(Key portKey) {
 740        String ip = config.getString(Key.SERVER_IP);
 741        int port = config.getInt(portKey);
 742        if (ip.isEmpty()) {
 743            return new InetSocketAddress(port);
 744        } else {
 745            return new InetSocketAddress(ip, port);
 746        }
 747    }
 748
 749    /**
 750     * Stops this server.
 751     */
 752    @Override
 753    public void shutdown() {
 754        // Just in case this gets called twice
 755        if (isShuttingDown) {
 756            return;
 757        }
 758        isShuttingDown = true;
 759        logger.info("The server is shutting down...");
 760
 761        // Disable plugins
 762        pluginManager.clearPlugins();
 763
 764        // Kick all players (this saves their data too)
 765        for (GlowPlayer player : new ArrayList<>(getRawOnlinePlayers())) {
 766            player.kickPlayer(getShutdownMessage(), false);
 767        }
 768
 769        // Stop the network servers - starts the shutdown process
 770        // It may take a second or two for Netty to totally clean up
 771        if (networkServer != null) {
 772            networkServer.shutdown();
 773        }
 774        if (queryServer != null) {
 775            queryServer.shutdown();
 776        }
 777        if (rconServer != null) {
 778            rconServer.shutdown();
 779        }
 780
 781        // Save worlds
 782        for (World world : getWorlds()) {
 783            logger.info("Saving world: " + world.getName());
 784            unloadWorld(world, true);
 785        }
 786
 787        // Stop scheduler and console
 788        scheduler.stop();
 789        consoleManager.stop();
 790
 791        // Wait for a while and terminate any rogue threads
 792        new ShutdownMonitorThread().start();
 793    }
 794
 795    /**
 796     * Load the server configuration.
 797     */
 798    private void loadConfig() {
 799        config.load();
 800        worldConfig.load();
 801
 802        // modifiable values
 803        spawnRadius = config.getInt(Key.SPAWN_RADIUS);
 804        whitelistEnabled = config.getBoolean(Key.WHITELIST);
 805        idleTimeout = config.getInt(Key.PLAYER_IDLE_TIMEOUT);
 806        craftingManager.initialize();
 807
 808        // special handling
 809        warnState = WarningState.value(config.getString(Key.WARNING_STATE));
 810        try {
 811            defaultGameMode = GameMode.valueOf(config.getString(Key.GAMEMODE));
 812        } catch (IllegalArgumentException | NullPointerException e) {
 813            defaultGameMode = GameMode.SURVIVAL;
 814        }
 815
 816        // server icon
 817        defaultIcon = new GlowServerIcon();
 818        try {
 819            File file = config.getFile("server-icon.png");
 820            if (file.isFile()) {
 821                defaultIcon = new GlowServerIcon(file);
 822            }
 823        } catch (Exception e) {
 824            logger.log(Level.WARNING, "Failed to load server-icon.png", e);
 825        }
 826    }
 827
 828    /**
 829     * Loads all plugins, calling onLoad, &c.
 830     */
 831    private void loadPlugins() {
 832        // clear the map
 833        commandMap.clearCommands();
 834        // glowstone commands
 835        commandMap.register("glowstone", new ColorCommand());
 836        commandMap.register("glowstone", new GlowstoneCommand());
 837        // vanilla commands
 838        commandMap.register("minecraft", new TellrawCommand());
 839        commandMap.register("minecraft", new TitleCommand());
 840        commandMap.register("minecraft", new TeleportCommand());
 841        commandMap.register("minecraft", new SummonCommand());
 842        commandMap.register("minecraft", new WorldBorderCommand());
 843        commandMap.register("minecraft", new SayCommand());
 844        commandMap.register("minecraft", new StopCommand());
 845        commandMap.register("minecraft", new OpCommand());
 846        commandMap.register("minecraft", new GameModeCommand());
 847        commandMap.register("minecraft", new FunctionCommand());
 848        commandMap.register("minecraft", new DeopCommand());
 849        commandMap.register("minecraft", new KickCommand());
 850        commandMap.register("minecraft", new GameRuleCommand());
 851        commandMap.register("minecraft", new TellCommand());
 852        commandMap.register("minecraft", new ListCommand());
 853        commandMap.register("minecraft", new BanCommand());
 854        commandMap.register("minecraft", new BanIpCommand());
 855        commandMap.register("minecraft", new BanListCommand());
 856        commandMap.register("minecraft", new GiveCommand());
 857        commandMap.register("minecraft", new DifficultyCommand());
 858        commandMap.register("minecraft", new KillCommand());
 859        commandMap.register("minecraft", new PardonCommand());
 860        commandMap.register("minecraft", new PardonIpCommand());
 861        commandMap.register("minecraft", new WhitelistCommand());
 862        commandMap.register("minecraft", new TimeCommand());
 863        commandMap.register("minecraft", new WeatherCommand());
 864        commandMap.register("minecraft", new SaveAllCommand());
 865        commandMap.register("minecraft", new SaveToggleCommand(true));
 866        commandMap.register("minecraft", new SaveToggleCommand(false));
 867        commandMap.register("minecraft", new ClearCommand());
 868        commandMap.register("minecraft", new TpCommand());
 869        commandMap.register("minecraft", new MeCommand());
 870        commandMap.register("minecraft", new SeedCommand());
 871        commandMap.register("minecraft", new XpCommand());
 872        commandMap.register("minecraft", new DefaultGameModeCommand());
 873        commandMap.register("minecraft", new SetIdleTimeoutCommand());
 874        commandMap.register("minecraft", new SpawnPointCommand());
 875        commandMap.register("minecraft", new ToggleDownfallCommand());
 876        commandMap.register("minecraft", new SetWorldSpawnCommand());
 877        commandMap.register("minecraft", new PlaySoundCommand());
 878        commandMap.register("minecraft", new EffectCommand());
 879        commandMap.register("minecraft", new EnchantCommand());
 880        commandMap.register("minecraft", new TestForCommand());
 881        commandMap.register("minecraft", new TestForBlockCommand());
 882        commandMap.register("minecraft", new SetBlockCommand());
 883
 884        File folder = new File(config.getString(Key.PLUGIN_FOLDER));
 885        if (!folder.isDirectory() && !folder.mkdirs()) {
 886            logger.log(Level.SEVERE, "Could not create plugins directory: " + folder);
 887        }
 888
 889        // detect plugin types
 890        pluginTypeDetector = new GlowPluginTypeDetector(folder);
 891        pluginTypeDetector.scan();
 892
 893        // clear plugins and prepare to load (Bukkit)
 894        pluginManager.clearPlugins();
 895        pluginManager.registerInterface(JavaPluginLoader.class);
 896        Plugin[] plugins = pluginManager.loadPlugins(folder.getPath(), pluginTypeDetector.bukkitPlugins.toArray(new File[pluginTypeDetector.bukkitPlugins.size()]));
 897
 898        // call onLoad methods
 899        for (Plugin plugin : plugins) {
 900            try {
 901                plugin.onLoad();
 902            } catch (Exception ex) {
 903                logger.log(Level.SEVERE, "Error loading " + plugin.getDescription().getFullName(), ex);
 904            }
 905        }
 906
 907        if (!pluginTypeDetector.spongePlugins.isEmpty()) {
 908            boolean hasSponge = false;
 909            for (Plugin plugin : plugins) {
 910                if (plugin.getName().equals("Bukkit2Sponge")) {
 911                    hasSponge = true; // TODO: better detection method, plugin description file annotation APIs?
 912                    break;
 913                }
 914            }
 915
 916            boolean spongeOnlyPlugins = false;
 917            for (File spongePlugin : pluginTypeDetector.spongePlugins) {
 918                if (!pluginTypeDetector.bukkitPlugins.contains(spongePlugin)) {
 919                    spongeOnlyPlugins = true;
 920                }
 921            }
 922
 923            if (!hasSponge && spongeOnlyPlugins) {
 924                logger.log(Level.WARNING, "SpongeAPI plugins found, but no Sponge bridge present! They will be ignored.");
 925                for (File file : getSpongePlugins()) {
 926                    logger.log(Level.WARNING, "Ignored SpongeAPI plugin: " + file.getPath());
 927                }
 928                logger.log(Level.WARNING, "Suggestion: install https://github.com/GlowstoneMC/Bukkit2Sponge to load these plugins");
 929            }
 930        }
 931
 932        if (!pluginTypeDetector.canaryPlugins.isEmpty() ||
 933                !pluginTypeDetector.forgefPlugins.isEmpty() ||
 934                !pluginTypeDetector.forgenPlugins.isEmpty() ||
 935                !pluginTypeDetector.unrecognizedPlugins.isEmpty()) {
 936            logger.log(Level.WARNING, "Unsupported plugin types found, will be ignored:");
 937
 938            for (File file : pluginTypeDetector.canaryPlugins)
 939                logger.log(Level.WARNING, "Canary plugin not supported: " + file.getPath());
 940
 941            for (File file : pluginTypeDetector.forgefPlugins)
 942                logger.log(Level.WARNING, "Forge plugin not supported: " + file.getPath());
 943
 944            for (File file : pluginTypeDetector.forgenPlugins)
 945                logger.log(Level.WARNING, "Forge plugin not supported: " + file.getPath());
 946
 947            for (File file : pluginTypeDetector.unrecognizedPlugins)
 948                logger.log(Level.WARNING, "Unrecognized plugin not supported: " + file.getPath());
 949        }
 950
 951    }
 952
 953    /**
 954     * A list of detected files that are Sponge plugins.
 955     *
 956     * @return a list of {@link File Files} that are Sponge plugins.
 957     */
 958    public List<File> getSpongePlugins() {
 959        return pluginTypeDetector.spongePlugins;
 960    }
 961
 962    /**
 963     * Enable all plugins of the given load order type.
 964     *
 965     * @param type The type of plugin to enable.
 966     */
 967    private void enablePlugins(PluginLoadOrder type) {
 968        if (type == PluginLoadOrder.STARTUP) {
 969            helpMap.clear();
 970            helpMap.loadConfig(config.getConfigFile(Key.HELP_FILE));
 971        }
 972
 973        // load all the plugins
 974        Plugin[] plugins = pluginManager.getPlugins();
 975        for (Plugin plugin : plugins) {
 976            if (!plugin.isEnabled() && plugin.getDescription().getLoad() == type) {
 977                List<Permission> perms = plugin.getDescription().getPermissions();
 978                for (Permission perm : perms) {
 979                    try {
 980                        pluginManager.addPermission(perm);
 981                    } catch (IllegalArgumentException ex) {
 982                        getLogger().log(Level.WARNING, "Plugin " + plugin.getDescription().getFullName() + " tried to register permission '" + perm.getName() + "' but it's already registered", ex);
 983                    }
 984                }
 985
 986                try {
 987                    pluginManager.enablePlugin(plugin);
 988                } catch (Throwable ex) {
 989                    logger.log(Level.SEVERE, "Error loading " + plugin.getDescription().getFullName(), ex);
 990                }
 991            }
 992        }
 993
 994        if (type == PluginLoadOrder.POSTWORLD) {
 995            commandMap.setFallbackCommands();
 996            commandMap.registerServerAliases();
 997            DefaultPermissions.registerCorePermissions();
 998            // Default permissions
 999            this.permissionRoot = DefaultPermissions.registerPermission("minecraft", "Gives the user the ability to use all Minecraft utilities and commands");
1000            this.permissionCommand = DefaultPermissions.registerPermission("minecraft.command", "Gives the user the ability to use all Minecraft commands", permissionRoot);
1001            DefaultPermissions.registerPermission("minecraft.command.tell", "Allows the user to send a private message", PermissionDefault.TRUE, permissionCommand);
1002            permissionCommand.recalculatePermissibles();
1003            permissionRoot.recalculatePermissibles();
1004            helpMap.initializeCommands();
1005            helpMap.amendTopics(config.getConfigFile(Key.HELP_FILE));
1006
1007            // load permissions.yml
1008            ConfigurationSection permConfig = config.getConfigFile(Key.PERMISSIONS_FILE);
1009
1010            Map<String, Map<String, Object>> data = new HashMap<>();
1011
1012            permConfig.getValues(false).forEach((key, value) -> data.put(key, ((MemorySection) value).getValues(false)));
1013
1014            List<Permission> perms = Permission.loadPermissions(data, "Permission node '%s' in permissions config is invalid", PermissionDefault.OP);
1015
1016            for (Permission perm : perms) {
1017                try {
1018                    pluginManager.addPermission(perm);
1019                } catch (IllegalArgumentException ex) {
1020                    getLogger().log(Level.WARNING, "Permission config tried to register '" + perm.getName() + "' but it's already registered", ex);
1021                }
1022            }
1023        }
1024    }
1025
1026    /**
1027     * Reloads the server, refreshing settings and plugin information
1028     */
1029    @Override
1030    public void reload() {
1031        try {
1032            // Reload relevant configuration
1033            loadConfig();
1034            opsList.load();
1035            whitelist.load();
1036            nameBans.load();
1037            ipBans.load();
1038
1039            // Reset crafting
1040            craftingManager.resetRecipes();
1041
1042            // Load plugins
1043            loadPlugins();
1044            enablePlugins(PluginLoadOrder.STARTUP);
1045            enablePlugins(PluginLoadOrder.POSTWORLD);
1046        } catch (Exception ex) {
1047            logger.log(Level.SEVERE, "Uncaught error while reloading", ex);
1048        }
1049    }
1050
1051    @Override
1052    public void reloadData() {
1053
1054    }
1055
1056    @Override
1057    public String toString() {
1058        return "GlowServer{name=" + getName() + ",version=" + getVersion() + ",minecraftVersion=" + GAME_VERSION + "}";
1059    }
1060
1061    ////////////////////////////////////////////////////////////////////////////
1062    // Access to internals
1063
1064    /**
1065     * Gets the command map.
1066     *
1067     * @return The {@link SimpleCommandMap}.
1068     */
1069    public SimpleCommandMap getCommandMap() {
1070        return commandMap;
1071    }
1072
1073    @Override
1074    public Advancement getAdvancement(NamespacedKey namespacedKey) {
1075        return advancements.get(namespacedKey);
1076    }
1077
1078    @Override
1079    public Iterator<Advancement> advancementIterator() {
1080        return Iterators.cycle(advancements.values());
1081    }
1082
1083    /**
1084     * Registers an advancement to the advancement registry.
1085     *
1086     * @param advancement the advancement to add.
1087     */
1088    public void addAdvancement(Advancement advancement) {
1089        advancements.put(advancement.getKey(), advancement);
1090    }
1091
1092    /**
1093     * Creates an {@link AdvancementsMessage} containing a list of advancements the server has, along with some extra actions.
1094     *
1095     * <p>This does not affect the server's advancement registry.
1096     *
1097     * @param clear  whether to clear the advancements on the player's perspective.
1098     * @param remove a list of advancement {@link NamespacedKey NamespacedKeys} to remove from the player's perspective.
1099     * @return a resulting {@link AdvancementsMessage} packet
1100     */
1101    public AdvancementsMessage createAdvancementsMessage(boolean clear, List<NamespacedKey> remove) {
1102        return createAdvancementsMessage(advancements, clear, remove);
1103    }
1104
1105    /**
1106     * Creates an {@link AdvancementsMessage} containing a given list of advancements, along with some extra actions.
1107     *
1108     * <p>This does not affect the server's advancement registry.
1109     *
1110     * @param advancements the advancements to add to the player's perspective.
1111     * @param clear        whether to clear the advancements on the player's perspective.
1112     * @param remove       a list of advancement {@link NamespacedKey NamespacedKeys} to remove from the player's perspective.
1113     * @return a resulting {@link AdvancementsMessage} packet
1114     */
1115    public AdvancementsMessage createAdvancementsMessage(Map<NamespacedKey, Advancement> advancements, boolean clear, List<NamespacedKey> remove) {
1116        return new AdvancementsMessage(clear, advancements, remove);
1117    }
1118
1119    /**
1120     * Gets the session registry.
1121     *
1122     * @return The {@link SessionRegistry}.
1123     */
1124    public SessionRegistry getSessionRegistry() {
1125        return sessions;
1126    }
1127
1128    /**
1129     * Gets the entity id manager.
1130     *
1131     * @return The {@link EntityIdManager}.
1132     */
1133    public EntityIdManager getEntityIdManager() {
1134        return entityIdManager;
1135    }
1136
1137    /**
1138     * Returns the list of operators on this server.
1139     *
1140     * @return A file containing a list of UUIDs for this server's operators.
1141     */
1142    public UuidListFile getOpsList() {
1143        return opsList;
1144    }
1145
1146    /**
1147     * Returns the list of whitelisted players on this server.
1148     *
1149     * @return A file containing a list of UUIDs for this server's whitelisted players.
1150     */
1151    public UuidListFile getWhitelist() {
1152        return whitelist;
1153    }
1154
1155    @Override
1156    public void setWhitelist(boolean enabled) {
1157        whitelistEnabled = enabled;
1158        config.set(Key.WHITELIST, whitelistEnabled);
1159        config.save();
1160    }
1161
1162    /**
1163     * Returns the folder where configuration files are stored
1164     *
1165     * @return The server's configuration folder.
1166     */
1167    public File getConfigDir() {
1168        return config.getDirectory();
1169    }
1170
1171    /**
1172     * Return the crafting manager.
1173     *
1174     * @return The server's crafting manager.
1175     */
1176    public CraftingManager getCraftingManager() {
1177        return craftingManager;
1178    }
1179
1180    /**
1181     * The key pair generated at server start up
1182     *
1183     * @return The key pair generated at server start up
1184     */
1185    public KeyPair getKeyPair() {
1186        return keyPair;
1187    }
1188
1189    /**
1190     * Returns the player data service attached to the first world.
1191     *
1192     * @return The server's player data service.
1193     */
1194    public PlayerDataService getPlayerDataService() {
1195        return worlds.getWorlds().get(0).getStorage().getPlayerDataService();
1196    }
1197
1198    /**
1199     * Returns the scoreboard I/O service attached to the first world.
1200     *
1201     * @return The server's scoreboard I/O service
1202     */
1203    public ScoreboardIoService getScoreboardIoService() {
1204        return worlds.getWorlds().get(0).getStorage().getScoreboardIoService();
1205    }
1206
1207    /**
1208     * Returns the player statitics I/O service attached to the first world.
1209     *
1210     * @return the server's statistics I/O service
1211     */
1212    public PlayerStatisticIoService getPlayerStatisticIoService() {
1213        return worlds.getWorlds().get(0).getStorage().getPlayerStatisticIoService();
1214    }
1215
1216    /**
1217     * Get the threshold to use for network compression defined in the config.
1218     *
1219     * @return The compression threshold, or -1 for no compression.
1220     */
1221    public int getCompressionThreshold() {
1222        return config.getInt(Key.COMPRESSION_THRESHOLD);
1223    }
1224
1225    /**
1226     * Get the default game difficulty defined in the config.
1227     *
1228     * @return The default difficulty.
1229     */
1230    public Difficulty getDifficulty() {
1231        try {
1232            return Difficulty.valueOf(config.getString(Key.DIFFICULTY));
1233        } catch (IllegalArgumentException | NullPointerException e) {
1234            return Difficulty.NORMAL;
1235        }
1236    }
1237
1238    /**
1239     * Get whether worlds should keep their spawns loaded by default.
1240     *
1241     * @return Whether to keep spawns loaded by default.
1242     */
1243    public boolean keepSpawnLoaded() {
1244        return config.getBoolean(Key.PERSIST_SPAWN);
1245    }
1246
1247    /**
1248     * Get whether to populate chunks when they are anchored.
1249     *
1250     * @return Whether to populate chunks when they are anchored.
1251     */
1252    public boolean populateAnchoredChunks() {
1253        return config.getBoolean(Key.POPULATE_ANCHORED_CHUNKS);
1254    }
1255
1256    /**
1257     * Get whether parsing of data provided by a proxy is enabled.
1258     *
1259     * @return True if a proxy is providing data to use.
1260     */
1261    public boolean getProxySupport() {
1262        return config.getBoolean(Key.PROXY_SUPPORT);
1263    }
1264
1265    /**
1266     * Get whether to use color codes in Rcon responses.
1267     *
1268     * @return True if color codes will be present in Rcon responses
1269     */
1270    public boolean useRconColors() {
1271        return config.getBoolean(Key.RCON_COLORS);
1272    }
1273
1274    /**
1275     * Gets the {@link MaterialValueManager} for this server.
1276     *
1277     * @return the {@link MaterialValueManager} for this server.
1278     */
1279    public MaterialValueManager getMaterialValueManager() {
1280        return materialValueManager;
1281    }
1282
1283    /**
1284     * Gets the {@link BossBarManager} for this server.
1285     *
1286     * @return the {@link BossBarManager} for this server.
1287     */
1288    public BossBarManager getBossBarManager() {
1289        return bossBarManager;
1290    }
1291
1292    /**
1293     * Get the resource pack url for this server, or {@code null} if not set.
1294     *
1295     * @return The url of the resource pack to use, or {@code null}
1296     */
1297    public String getResourcePackURL() {
1298        return config.getString(Key.RESOURCE_PACK);
1299    }
1300
1301    /**
1302     * Get the resource pack hash for this server, or the empty string if not set.
1303     *
1304     * @return The hash of the resource pack, or the empty string
1305     */
1306    public String getResourcePackHash() {
1307        return config.getString(Key.RESOURCE_PACK_HASH);
1308    }
1309
1310    /**
1311     * Get whether achievements should be announced.
1312     *
1313     * @return True if achievements should be announced in chat.
1314     */
1315    public boolean getAnnounceAchievements() {
1316        return config.getBoolean(Key.ANNOUNCE_ACHIEVEMENTS);
1317    }
1318
1319    ////////////////////////////////////////////////////////////////////////////
1320    // St