content: package org.bukkit.craftbukkit;
  
  import java.awt.image.BufferedImage;
  import java.io.File;
  import java.io.FileInputStream;
  import java.io.FileNotFoundException;
  import java.io.IOException;
  import java.io.InputStreamReader;
  import java.util.ArrayList;
  import java.util.Arrays;
  import java.util.Collections;
  import java.util.HashSet;
  import java.util.Iterator;
  import java.util.LinkedHashMap;
  import java.util.LinkedHashSet;
  import java.util.List;
  import java.util.Map;
  import java.util.Set;
  import java.util.UUID;
  import java.util.logging.Level;
  import java.util.logging.Logger;
  import java.util.regex.Pattern;
  
  import javax.imageio.ImageIO;
  
  import net.minecraft.server.*;
  
  import org.bukkit.BanList;
  import org.bukkit.Bukkit;
  import org.bukkit.ChatColor;
  import org.bukkit.GameMode;
  import org.bukkit.Location;
  import org.bukkit.OfflinePlayer;
  import org.bukkit.Server;
  import org.bukkit.UnsafeValues;
  import org.bukkit.Warning.WarningState;
  import org.bukkit.World;
  import org.bukkit.World.Environment;
  import org.bukkit.WorldCreator;
  import org.bukkit.boss.BarColor;
  import org.bukkit.boss.BarFlag;
  import org.bukkit.boss.BarStyle;
  import org.bukkit.boss.BossBar;
  import org.bukkit.command.Command;
  import org.bukkit.command.CommandException;
  import org.bukkit.command.CommandSender;
  import org.bukkit.command.ConsoleCommandSender;
  import org.bukkit.command.PluginCommand;
  import org.bukkit.command.SimpleCommandMap;
  import org.bukkit.configuration.ConfigurationSection;
  import org.bukkit.configuration.file.YamlConfiguration;
  import org.bukkit.configuration.serialization.ConfigurationSerialization;
  import org.bukkit.conversations.Conversable;
  import org.bukkit.craftbukkit.boss.CraftBossBar;
  import org.bukkit.craftbukkit.command.VanillaCommandWrapper;
  import org.bukkit.craftbukkit.entity.CraftPlayer;
  import org.bukkit.craftbukkit.generator.CraftChunkData;
  import org.bukkit.craftbukkit.help.SimpleHelpMap;
  import org.bukkit.craftbukkit.inventory.CraftFurnaceRecipe;
  import org.bukkit.craftbukkit.inventory.CraftInventoryCustom;
  import org.bukkit.craftbukkit.inventory.CraftItemFactory;
  import org.bukkit.craftbukkit.inventory.CraftMerchantCustom;
  import org.bukkit.craftbukkit.inventory.CraftRecipe;
  import org.bukkit.craftbukkit.inventory.CraftShapedRecipe;
  import org.bukkit.craftbukkit.inventory.CraftShapelessRecipe;
  import org.bukkit.craftbukkit.inventory.RecipeIterator;
  import org.bukkit.craftbukkit.map.CraftMapView;
  import org.bukkit.craftbukkit.metadata.EntityMetadataStore;
  import org.bukkit.craftbukkit.metadata.PlayerMetadataStore;
  import org.bukkit.craftbukkit.metadata.WorldMetadataStore;
  import org.bukkit.craftbukkit.potion.CraftPotionBrewer;
  import org.bukkit.craftbukkit.scheduler.CraftScheduler;
  import org.bukkit.craftbukkit.scoreboard.CraftScoreboardManager;
  import org.bukkit.craftbukkit.util.CraftIconCache;
  import org.bukkit.craftbukkit.util.CraftMagicNumbers;
  import org.bukkit.craftbukkit.util.DatFileFilter;
  import org.bukkit.craftbukkit.util.Versioning;
  import org.bukkit.craftbukkit.util.permissions.CraftDefaultPermissions;
  import org.bukkit.entity.Entity;
  import org.bukkit.entity.Player;
  import org.bukkit.event.command.UnknownCommandEvent; // Paper
  import org.bukkit.event.inventory.InventoryType;
  import org.bukkit.event.player.PlayerChatTabCompleteEvent;
  import org.bukkit.event.server.BroadcastMessageEvent;
  import org.bukkit.event.world.WorldInitEvent;
  import org.bukkit.event.world.WorldLoadEvent;
  import org.bukkit.event.world.WorldUnloadEvent;
  import org.bukkit.generator.ChunkGenerator;
  import org.bukkit.help.HelpMap;
  import org.bukkit.inventory.FurnaceRecipe;
  import org.bukkit.inventory.ItemStack;
  import org.bukkit.inventory.Merchant;
  import org.bukkit.inventory.Inventory;
  import org.bukkit.inventory.InventoryHolder;
  import org.bukkit.inventory.Recipe;
  import org.bukkit.inventory.ShapedRecipe;
  import org.bukkit.inventory.ShapelessRecipe;
  import org.bukkit.permissions.Permissible;
  import org.bukkit.permissions.Permission;
  import org.bukkit.plugin.Plugin;
  import org.bukkit.plugin.PluginLoadOrder;
  import org.bukkit.plugin.PluginManager;
  import org.bukkit.plugin.ServicesManager;
  import org.bukkit.plugin.SimplePluginManager;
  import org.bukkit.plugin.SimpleServicesManager;
  import org.bukkit.plugin.java.JavaPluginLoader;
  import org.bukkit.plugin.messaging.Messenger;
  import org.bukkit.potion.Potion;
  import org.bukkit.potion.PotionEffectType;
  import org.bukkit.plugin.messaging.StandardMessenger;
  import org.bukkit.scheduler.BukkitWorker;
  import org.bukkit.util.StringUtil;
  import org.bukkit.util.permissions.DefaultPermissions;
  import org.yaml.snakeyaml.Yaml;
  import org.yaml.snakeyaml.constructor.SafeConstructor;
  import org.yaml.snakeyaml.error.MarkedYAMLException;
  import org.apache.commons.lang.Validate;
  
  import com.google.common.base.Charsets;
  import com.google.common.base.Function;
  import com.google.common.base.Preconditions;
  import com.google.common.collect.ImmutableList;
  import com.google.common.collect.Iterators;
  import com.google.common.collect.Lists;
  import com.google.common.collect.MapMaker;
  import com.mojang.authlib.GameProfile;
  
  import io.netty.buffer.ByteBuf;
  import io.netty.buffer.ByteBufOutputStream;
  import io.netty.buffer.Unpooled;
  import io.netty.handler.codec.base64.Base64;
  import org.apache.commons.lang.StringUtils;
  import org.bukkit.NamespacedKey;
  import org.bukkit.craftbukkit.util.CraftNamespacedKey;
  import org.bukkit.event.server.TabCompleteEvent;
  import net.md_5.bungee.api.chat.BaseComponent;
  
  import javax.annotation.Nullable; // Paper
  import javax.annotation.Nonnull; // Paper
  
  /**
   * Akarin Changes Note
   * 1) Guava -> Caffeine (performance)
   */
  public final class CraftServer implements Server {
      private final String serverName = "Paper";
      private final String serverVersion;
      private final String bukkitVersion = Versioning.getBukkitVersion();
      private final Logger logger = Logger.getLogger("Minecraft");
      private final ServicesManager servicesManager = new SimpleServicesManager();
      private final CraftScheduler scheduler = new CraftScheduler();
      private final SimpleCommandMap commandMap = new SimpleCommandMap(this);
      private final SimpleHelpMap helpMap = new SimpleHelpMap(this);
      private final StandardMessenger messenger = new StandardMessenger();
      private final SimplePluginManager pluginManager = new SimplePluginManager(this, commandMap);
      protected final MinecraftServer console;
      protected final DedicatedPlayerList playerList;
      private final Map<String, World> worlds = new LinkedHashMap<String, World>();
      private YamlConfiguration configuration;
      private YamlConfiguration commandsConfiguration;
      private final Yaml yaml = new Yaml(new SafeConstructor());
      private final com.github.benmanes.caffeine.cache.Cache<UUID, OfflinePlayer> offlinePlayers = com.github.benmanes.caffeine.cache.Caffeine.newBuilder().weakValues().build(); // Akarin - caffeine
      private final EntityMetadataStore entityMetadata = new EntityMetadataStore();
      private final PlayerMetadataStore playerMetadata = new PlayerMetadataStore();
      private final WorldMetadataStore worldMetadata = new WorldMetadataStore();
      private int monsterSpawn = -1;
      private int animalSpawn = -1;
      private int waterAnimalSpawn = -1;
      private int ambientSpawn = -1;
      public int chunkGCPeriod = -1;
      public int chunkGCLoadThresh = 0;
      private File container;
      private WarningState warningState = WarningState.DEFAULT;
      private final BooleanWrapper online = new BooleanWrapper();
      public CraftScoreboardManager scoreboardManager;
      public boolean playerCommandState;
      private boolean printSaveWarning;
      private CraftIconCache icon;
      private boolean overrideAllCommandBlockCommands = false;
      private boolean unrestrictedAdvancements;
      private boolean unrestrictedSignCommands; // Paper
      private final List<CraftPlayer> playerView;
      public int reloadCount;
      public static Exception excessiveVelEx; // Paper - Velocity warnings
  
      private final class BooleanWrapper {
          private boolean value = true;
      }
  
      static {
          ConfigurationSerialization.registerClass(CraftOfflinePlayer.class);
          CraftItemFactory.instance();
      }
  
      public CraftServer(MinecraftServer console, PlayerList playerList) {
          this.console = console;
          this.playerList = (DedicatedPlayerList) playerList;
          this.playerView = Collections.unmodifiableList(Lists.transform(playerList.players, new Function<EntityPlayer, CraftPlayer>() {
              @Override
              public CraftPlayer apply(EntityPlayer player) {
                  return player.getBukkitEntity();
              }
          }));
          this.serverVersion = CraftServer.class.getPackage().getImplementationVersion();
          online.value = console.getPropertyManager().getBoolean("online-mode", true);
  
          Bukkit.setServer(this);
  
          // Register all the Enchantments and PotionTypes now so we can stop new registration immediately after
          Enchantments.DAMAGE_ALL.getClass();
          org.bukkit.enchantments.Enchantment.stopAcceptingRegistrations();
  
          Potion.setPotionBrewer(new CraftPotionBrewer());
          MobEffects.BLINDNESS.getClass();
          PotionEffectType.stopAcceptingRegistrations();
          // Ugly hack :(
  
          if (!Main.useConsole) {
              getLogger().info("Console input is disabled due to --noconsole command argument");
          }
  
          configuration = YamlConfiguration.loadConfiguration(getConfigFile());
          configuration.options().copyDefaults(true);
          configuration.setDefaults(YamlConfiguration.loadConfiguration(new InputStreamReader(getClass().getClassLoader().getResourceAsStream("configurations/bukkit.yml"), Charsets.UTF_8)));
          ConfigurationSection legacyAlias = null;
          if (!configuration.isString("aliases")) {
              legacyAlias = configuration.getConfigurationSection("aliases");
              configuration.set("aliases", "now-in-commands.yml");
          }
          saveConfig();
          if (getCommandsConfigFile().isFile()) {
              legacyAlias = null;
          }
          commandsConfiguration = YamlConfiguration.loadConfiguration(getCommandsConfigFile());
          commandsConfiguration.options().copyDefaults(true);
          commandsConfiguration.setDefaults(YamlConfiguration.loadConfiguration(new InputStreamReader(getClass().getClassLoader().getResourceAsStream("configurations/commands.yml"), Charsets.UTF_8)));
          saveCommandsConfig();
  
          // Migrate aliases from old file and add previously implicit $1- to pass all arguments
          if (legacyAlias != null) {
              ConfigurationSection aliases = commandsConfiguration.createSection("aliases");
              for (String key : legacyAlias.getKeys(false)) {
                  ArrayList<String> commands = new ArrayList<String>();
  
                  if (legacyAlias.isList(key)) {
                      for (String command : legacyAlias.getStringList(key)) {
                          commands.add(command + " $1-");
                      }
                  } else {
                      commands.add(legacyAlias.getString(key) + " $1-");
                  }
  
                  aliases.set(key, commands);
              }
          }
  
          saveCommandsConfig();
          overrideAllCommandBlockCommands = commandsConfiguration.getStringList("command-block-overrides").contains("*");
          unrestrictedAdvancements = commandsConfiguration.getBoolean("unrestricted-advancements");
          // Paper start
          unrestrictedSignCommands = commandsConfiguration.getBoolean("unrestricted-signs");
          if (unrestrictedSignCommands) {
              logger.warning("Warning: Commands are no longer restricted on signs. If you allow players to use Creative Mode, there may be risk of players bypassing permissions. Use this setting at your own risk!!!!");
          }
          // Paper end
          pluginManager.useTimings(configuration.getBoolean("settings.plugin-profiling"));
          monsterSpawn = configuration.getInt("spawn-limits.monsters");
          animalSpawn = configuration.getInt("spawn-limits.animals");
          waterAnimalSpawn = configuration.getInt("spawn-limits.water-animals");
          ambientSpawn = configuration.getInt("spawn-limits.ambient");
          console.autosavePeriod = configuration.getInt("ticks-per.autosave");
          warningState = WarningState.value(configuration.getString("settings.deprecated-verbose"));
          chunkGCPeriod = configuration.getInt("chunk-gc.period-in-ticks");
          chunkGCLoadThresh = configuration.getInt("chunk-gc.load-threshold");
          loadIcon();
      }
  
      public boolean getPermissionOverride(ICommandListener listener) {
          while (listener instanceof CommandListenerWrapper) {
              listener = ((CommandListenerWrapper) listener).base;
          }
  
          if (unrestrictedSignCommands && listener instanceof TileEntitySign.ISignCommandListener) return true; // Paper
          return unrestrictedAdvancements && listener instanceof AdvancementRewards.AdvancementCommandListener;
      }
  
      public boolean getCommandBlockOverride(String command) {
          return overrideAllCommandBlockCommands || commandsConfiguration.getStringList("command-block-overrides").contains(command);
      }
  
      private File getConfigFile() {
          return (File) console.options.valueOf("bukkit-settings");
      }
  
      private File getCommandsConfigFile() {
          return (File) console.options.valueOf("commands-settings");
      }
  
      private void saveConfig() {
          try {
              configuration.save(getConfigFile());
          } catch (IOException ex) {
              Logger.getLogger(CraftServer.class.getName()).log(Level.SEVERE, "Could not save " + getConfigFile(), ex);
          }
      }
  
      private void saveCommandsConfig() {
          try {
              commandsConfiguration.save(getCommandsConfigFile());
          } catch (IOException ex) {
              Logger.getLogger(CraftServer.class.getName()).log(Level.SEVERE, "Could not save " + getCommandsConfigFile(), ex);
          }
      }
  
      public void loadPlugins() {
          pluginManager.registerInterface(JavaPluginLoader.class);
  
          File pluginFolder = (File) console.options.valueOf("plugins");
  
          if (pluginFolder.exists()) {
              Plugin[] plugins = pluginManager.loadPlugins(pluginFolder);
              for (Plugin plugin : plugins) {
                  try {
                      String message = String.format("Loading %s", plugin.getDescription().getFullName());
                      plugin.getLogger().info(message);
                      plugin.onLoad();
                  } catch (Throwable ex) {
                      Logger.getLogger(CraftServer.class.getName()).log(Level.SEVERE, ex.getMessage() + " initializing " + plugin.getDescription().getFullName() + " (Is it up to date?)", ex);
                  }
              }
          } else {
              pluginFolder.mkdir();
          }
      }
  
      public void enablePlugins(PluginLoadOrder type) {
          if (type == PluginLoadOrder.STARTUP) {
              helpMap.clear();
              helpMap.initializeGeneralTopics();
              if (com.destroystokyo.paper.PaperConfig.loadPermsBeforePlugins) loadCustomPermissions(); // Paper
          }
  
          Plugin[] plugins = pluginManager.getPlugins();
  
          for (Plugin plugin : plugins) {
              if ((!plugin.isEnabled()) && (plugin.getDescription().getLoad() == type)) {
                  enablePlugin(plugin);
              }
          }
  
          if (type == PluginLoadOrder.POSTWORLD) {
              // Spigot start - Allow vanilla commands to be forced to be the main command
              setVanillaCommands(true);
              commandMap.setFallbackCommands();
              setVanillaCommands(false);
              // Spigot end
              commandMap.registerServerAliases();
              if (!com.destroystokyo.paper.PaperConfig.loadPermsBeforePlugins) loadCustomPermissions(); // Paper
              DefaultPermissions.registerCorePermissions();
              CraftDefaultPermissions.registerCorePermissions();
              helpMap.initializeCommands();
          }
      }
  
      public void disablePlugins() {
          pluginManager.disablePlugins();
      }
  
      private void setVanillaCommands(boolean first) { // Spigot
          Map<String, ICommand> commands = new CommandDispatcher(console).getCommands();
          for (ICommand cmd : commands.values()) {
              // Spigot start
              VanillaCommandWrapper wrapper = new VanillaCommandWrapper((CommandAbstract) cmd, LocaleI18n.get(cmd.getUsage(null)));
              if (org.spigotmc.SpigotConfig.replaceCommands.contains( wrapper.getName() ) ) {
                  if (first) {
                      commandMap.register("minecraft", wrapper);
                  }
              } else if (!first) {
                  commandMap.register("minecraft", wrapper);
              }
              // Spigot end
          }
      }
  
      private void enablePlugin(Plugin plugin) {
          try {
              List<Permission> perms = plugin.getDescription().getPermissions();
  
              for (Permission perm : perms) {
                  try {
                      pluginManager.addPermission(perm, false);
                  } catch (IllegalArgumentException ex) {
                      getLogger().log(Level.WARNING, "Plugin " + plugin.getDescription().getFullName() + " tried to register permission '" + perm.getName() + "' but it's already registered", ex);
                  }
              }
              pluginManager.dirtyPermissibles();
  
              pluginManager.enablePlugin(plugin);
          } catch (Throwable ex) {
              Logger.getLogger(CraftServer.class.getName()).log(Level.SEVERE, ex.getMessage() + " loading " + plugin.getDescription().getFullName() + " (Is it up to date?)", ex);
          }
      }
  
      @Override
      public String getName() {
          return serverName;
      }
  
      @Override
      public String getVersion() {
          return serverVersion + " (MC: " + console.getVersion() + ")";
      }
  
      @Override
      public String getBukkitVersion() {
          return bukkitVersion;
      }
  
      @Override
      public List<CraftPlayer> getOnlinePlayers() {
          return this.playerView;
      }
  
      @Override
      @Deprecated
      public Player getPlayer(final String name) {
          Validate.notNull(name, "Name cannot be null");
  
          Player found = getPlayerExact(name);
          // Try for an exact match first.
          if (found != null) {
              return found;
          }
  
          String lowerName = name.toLowerCase(java.util.Locale.ENGLISH);
          int delta = Integer.MAX_VALUE;
          for (Player player : getOnlinePlayers()) {
              if (player.getName().toLowerCase(java.util.Locale.ENGLISH).startsWith(lowerName)) {
                  int curDelta = Math.abs(player.getName().length() - lowerName.length());
                  if (curDelta < delta) {
                      found = player;
                      delta = curDelta;
                  }
                  if (curDelta == 0) break;
              }
          }
          return found;
      }
  
      @Override
      @Deprecated
      public Player getPlayerExact(String name) {
          Validate.notNull(name, "Name cannot be null");
  
          EntityPlayer player = playerList.getPlayer(name);
          return (player != null) ? player.getBukkitEntity() : null;
      }
  
      @Override
      public Player getPlayer(UUID id) {
          EntityPlayer player = playerList.a(id);
  
          if (player != null) {
              return player.getBukkitEntity();
          }
  
          return null;
      }
  
      @Override
      public int broadcastMessage(String message) {
          return broadcast(message, BROADCAST_CHANNEL_USERS);
      }
  
      public Player getPlayer(final EntityPlayer entity) {
          return entity.getBukkitEntity();
      }
  
      @Override
      @Deprecated
      public List<Player> matchPlayer(String partialName) {
          Validate.notNull(partialName, "PartialName cannot be null");
  
          List<Player> matchedPlayers = new ArrayList<Player>();
  
          for (Player iterPlayer : this.getOnlinePlayers()) {
              String iterPlayerName = iterPlayer.getName();
  
              if (partialName.equalsIgnoreCase(iterPlayerName)) {
                  // Exact match
                  matchedPlayers.clear();
                  matchedPlayers.add(iterPlayer);
                  break;
              }
              if (iterPlayerName.toLowerCase(java.util.Locale.ENGLISH).contains(partialName.toLowerCase(java.util.Locale.ENGLISH))) {
                  // Partial match
                  matchedPlayers.add(iterPlayer);
              }
          }
  
          return matchedPlayers;
      }
  
      @Override
      public int getMaxPlayers() {
          return playerList.getMaxPlayers();
      }
  
      // NOTE: These are dependent on the corresponding call in MinecraftServer
      // so if that changes this will need to as well
      @Override
      public int getPort() {
          return this.getConfigInt("server-port", 25565);
      }
  
      @Override
      public int getViewDistance() {
          return this.getConfigInt("view-distance", 10);
      }
  
      @Override
      public String getIp() {
          return this.getConfigString("server-ip", "");
      }
  
      @Override
      public String getServerName() {
          return this.getConfigString("server-name", "Unknown Server");
      }
  
      @Override
      public String getServerId() {
          return this.getConfigString("server-id", "unnamed");
      }
  
      @Override
      public String getWorldType() {
          return this.getConfigString("level-type", "DEFAULT");
      }
  
      @Override
      public boolean getGenerateStructures() {
          return this.getConfigBoolean("generate-structures", true);
      }
  
      @Override
      public boolean getAllowEnd() {
          return this.configuration.getBoolean("settings.allow-end");
      }
  
      @Override
      public boolean getAllowNether() {
          return this.getConfigBoolean("allow-nether", true);
      }
  
      public boolean getWarnOnOverload() {
          return this.configuration.getBoolean("settings.warn-on-overload");
      }
  
      public boolean getQueryPlugins() {
          return this.configuration.getBoolean("settings.query-plugins");
      }
  
      @Override
      public boolean hasWhitelist() {
          return this.getConfigBoolean("white-list", false);
      }
  
      // NOTE: Temporary calls through to server.properies until its replaced
      private String getConfigString(String variable, String defaultValue) {
          return this.console.getPropertyManager().getString(variable, defaultValue);
      }
  
      private int getConfigInt(String variable, int defaultValue) {
          return this.console.getPropertyManager().getInt(variable, defaultValue);
      }
  
      private boolean getConfigBoolean(String variable, boolean defaultValue) {
          return this.console.getPropertyManager().getBoolean(variable, defaultValue);
      }
  
      // End Temporary calls
  
      @Override
      public String getUpdateFolder() {
          return this.configuration.getString("settings.update-folder", "update");
      }
  
      @Override
      public File getUpdateFolderFile() {
          return new File((File) console.options.valueOf("plugins"), this.configuration.getString("settings.update-folder", "update"));
      }
  
      @Override
      public long getConnectionThrottle() {
          // Spigot Start - Automatically set connection throttle for bungee configurations
          if (org.spigotmc.SpigotConfig.bungee) {
              return -1;
          } else {
              return this.configuration.getInt("settings.connection-throttle");
          }
          // Spigot End
      }
  
      @Override
      public int getTicksPerAnimalSpawns() {
          return this.configuration.getInt("ticks-per.animal-spawns");
      }
  
      @Override
      public int getTicksPerMonsterSpawns() {
          return this.configuration.getInt("ticks-per.monster-spawns");
      }
  
      @Override
      public PluginManager getPluginManager() {
          return pluginManager;
      }
  
      @Override
      public CraftScheduler getScheduler() {
          return scheduler;
      }
  
      @Override
      public ServicesManager getServicesManager() {
          return servicesManager;
      }
  
      @Override
      public List<World> getWorlds() {
          return new ArrayList<World>(worlds.values());
      }
  
      public DedicatedPlayerList getHandle() {
          return playerList;
      }
  
      // NOTE: Should only be called from DedicatedServer.ah()
      public boolean dispatchServerCommand(CommandSender sender, ServerCommand serverCommand) {
          if (sender instanceof Conversable) {
              Conversable conversable = (Conversable)sender;
  
              if (conversable.isConversing()) {
                  conversable.acceptConversationInput(serverCommand.command);
                  return true;
              }
          }
          try {
              this.playerCommandState = true;
              return dispatchCommand(sender, serverCommand.command);
          } catch (Exception ex) {
              getLogger().log(Level.WARNING, "Unexpected exception while parsing console command \"" + serverCommand.command + '"', ex);
              return false;
          } finally {
              this.playerCommandState = false;
          }
      }
  
      @Override
      public boolean dispatchCommand(CommandSender sender, String commandLine) {
          Validate.notNull(sender, "Sender cannot be null");
          Validate.notNull(commandLine, "CommandLine cannot be null");
  
          // Paper Start
          if (!org.spigotmc.AsyncCatcher.shuttingDown && !Bukkit.isPrimaryThread()) {
              final CommandSender fSender = sender;
              final String fCommandLine = commandLine;
              Bukkit.getLogger().log(Level.SEVERE, "Command Dispatched Async: " + commandLine);
              Bukkit.getLogger().log(Level.SEVERE, "Please notify author of plugin causing this execution to fix this bug! see: http://bit.ly/1oSiM6C", new Throwable());
              org.bukkit.craftbukkit.util.Waitable<Boolean> wait = new org.bukkit.craftbukkit.util.Waitable<Boolean>() {
                  @Override
                  protected Boolean evaluate() {
                      return dispatchCommand(fSender, fCommandLine);
                  }
              };
              net.minecraft.server.MinecraftServer.getServer().processQueue.add(wait);
              try {
                  return wait.get();
              } catch (InterruptedException e) {
                  Thread.currentThread().interrupt(); // This is proper habit for java. If we aren't handling it, pass it on!
              } catch (Exception e) {
                  throw new RuntimeException("Exception processing dispatch command", e.getCause());
              }
          }
          // Paper End
  
          if (commandMap.dispatch(sender, commandLine)) {
              return true;
          }
  
          // Spigot start
          if (StringUtils.isNotEmpty(org.spigotmc.SpigotConfig.unknownCommandMessage)) {
              // Paper start
              UnknownCommandEvent event = new UnknownCommandEvent(sender, commandLine, org.spigotmc.SpigotConfig.unknownCommandMessage);
              Bukkit.getServer().getPluginManager().callEvent(event);
              if (StringUtils.isNotEmpty(event.getMessage())) {
                  sender.sendMessage(event.getMessage());
              }
              // Paper end
          }
          // Spigot end
  
          return false;
      }
  
      @Override
      public void reload() {
          reloadCount++;
          configuration = YamlConfiguration.loadConfiguration(getConfigFile());
          commandsConfiguration = YamlConfiguration.loadConfiguration(getCommandsConfigFile());
          PropertyManager config = new PropertyManager(console.options);
  
          ((DedicatedServer) console).propertyManager = config;
  
          boolean animals = config.getBoolean("spawn-animals", console.getSpawnAnimals());
          boolean monsters = config.getBoolean("spawn-monsters", console.worlds.get(0).getDifficulty() != EnumDifficulty.PEACEFUL);
          EnumDifficulty difficulty = EnumDifficulty.getById(config.getInt("difficulty", console.worlds.get(0).getDifficulty().ordinal()));
  
          online.value = config.getBoolean("online-mode", console.getOnlineMode());
          console.setSpawnAnimals(config.getBoolean("spawn-animals", console.getSpawnAnimals()));
          console.setPVP(config.getBoolean("pvp", console.getPVP()));
          console.setAllowFlight(config.getBoolean("allow-flight", console.getAllowFlight()));
          console.setMotd(config.getString("motd", console.getMotd()));
          monsterSpawn = configuration.getInt("spawn-limits.monsters");
          animalSpawn = configuration.getInt("spawn-limits.animals");
          waterAnimalSpawn = configuration.getInt("spawn-limits.water-animals");
          ambientSpawn = configuration.getInt("spawn-limits.ambient");
          warningState = WarningState.value(configuration.getString("settings.deprecated-verbose"));
          printSaveWarning = false;
          console.autosavePeriod = configuration.getInt("ticks-per.autosave");
          chunkGCPeriod = configuration.getInt("chunk-gc.period-in-ticks");
          chunkGCLoadThresh = configuration.getInt("chunk-gc.load-threshold");
          loadIcon();
  
          try {
              playerList.getIPBans().load();
          } catch (IOException ex) {
              logger.log(Level.WARNING, "Failed to load banned-ips.json, " + ex.getMessage());
          }
          try {
              playerList.getProfileBans().load();
          } catch (IOException ex) {
              logger.log(Level.WARNING, "Failed to load banned-players.json, " + ex.getMessage());
          }
  
          org.spigotmc.SpigotConfig.init((File) console.options.valueOf("spigot-settings")); // Spigot
          com.destroystokyo.paper.PaperConfig.init((File) console.options.valueOf("paper-settings")); // Paper
          for (WorldServer world : console.worlds) {
              world.worldData.setDifficulty(difficulty);
              world.setSpawnFlags(monsters, animals);
              if (this.getTicksPerAnimalSpawns() < 0) {
                  world.ticksPerAnimalSpawns = 400;
              } else {
                  world.ticksPerAnimalSpawns = this.getTicksPerAnimalSpawns();
              }
  
              if (this.getTicksPerMonsterSpawns() < 0) {
                  world.ticksPerMonsterSpawns = 1;
              } else {
                  world.ticksPerMonsterSpawns = this.getTicksPerMonsterSpawns();
              }
              world.spigotConfig.init(); // Spigot
              world.paperConfig.init(); // Paper
          }
  
          Plugin[] pluginClone = pluginManager.getPlugins().clone(); // Paper
          pluginManager.clearPlugins();
          commandMap.clearCommands();
  
          // Paper start
          for (Plugin plugin : pluginClone) {
              entityMetadata.removeAll(plugin);
              worldMetadata.removeAll(plugin);
              playerMetadata.removeAll(plugin);
          }
          // Paper end
  
          resetRecipes();
          reloadData();
          org.spigotmc.SpigotConfig.registerCommands(); // Spigot
          com.destroystokyo.paper.PaperConfig.registerCommands(); // Paper
          overrideAllCommandBlockCommands = commandsConfiguration.getStringList("command-block-overrides").contains("*");
  
          int pollCount = 0;
  
          // Wait for at most 2.5 seconds for plugins to close their threads
          while (pollCount < 50 && getScheduler().getActiveWorkers().size() > 0) {
              try {
                  Thread.sleep(50);
              } catch (InterruptedException e) {}
              pollCount++;
          }
  
          List<BukkitWorker> overdueWorkers = getScheduler().getActiveWorkers();
          for (BukkitWorker worker : overdueWorkers) {
              Plugin plugin = worker.getOwner();
              String author = "<NoAuthorGiven>";
              if (plugin.getDescription().getAuthors().size() > 0) {
                  author = plugin.getDescription().getAuthors().get(0);
              }
              getLogger().log(Level.SEVERE, String.format(
                  "Nag author: '%s' of '%s' about the following: %s",
                  author,
                  plugin.getDescription().getName(),
                  "This plugin is not properly shutting down its async tasks when it is being reloaded.  This may cause conflicts with the newly loaded version of the plugin"
              ));
          }
          loadPlugins();
          enablePlugins(PluginLoadOrder.STARTUP);
          enablePlugins(PluginLoadOrder.POSTWORLD);
      }
  
      @Override
      public void reloadData() {
          console.reload();
      }
  
      private void loadIcon() {
          icon = new CraftIconCache(null);
          try {
              final File file = new File(new File("."), "server-icon.png");
              if (file.isFile()) {
                  icon = loadServerIcon0(file);
              }
          } catch (Exception ex) {
              getLogger().log(Level.WARNING, "Couldn't load server icon", ex);
          }
      }
  
      @SuppressWarnings({ "unchecked", "finally" })
      private void loadCustomPermissions() {
          File file = new File(configuration.getString("settings.permissions-file"));
          FileInputStream stream;
  
          try {
              stream = new FileInputStream(file);
          } catch (FileNotFoundException ex) {
              try {
                  file.createNewFile();
              } finally {
                  return;
              }
          }
  
          Map<String, Map<String, Object>> perms;
  
          try {
              perms = (Map<String, Map<String, Object>>) yaml.load(stream);
          } catch (MarkedYAMLException ex) {
              getLogger().log(Level.WARNING, "Server permissions file " + file + " is not valid YAML: " + ex.toString());
              return;
          } catch (Throwable ex) {
              getLogger().log(Level.WARNING, "Server permissions file " + file + " is not valid YAML.", ex);
              return;
          } finally {
              try {
                  stream.close();
              } catch (IOException ex) {}
          }
  
          if (perms == null) {
              getLogger().log(Level.INFO, "Server permissions file " + file + " is empty, ignoring it");
              return;
          }
  
          List<Permission> permsList = Permission.loadPermissions(perms, "Permission node '%s' in " + file + " is invalid", Permission.DEFAULT_PERMISSION);
  
          for (Permission perm : permsList) {
              try {
                  pluginManager.addPermission(perm);
              } catch (IllegalArgumentException ex) {
                  getLogger().log(Level.SEVERE, "Permission in " + file + " was already defined", ex);
              }
          }
      }
  
      @Override
      public String toString() {
          return "CraftServer{" + "serverName=" + serverName + ",serverVersion=" + serverVersion + ",minecraftVersion=" + console.getVersion() + '}';
      }
  
      public World createWorld(String name, World.Environment environment) {
          return WorldCreator.name(name).environment(environment).createWorld();
      }
  
      public World createWorld(String name, World.Environment environment, long seed) {
          return WorldCreator.name(name).environment(environment).seed(seed).createWorld();
      }
  
      public World createWorld(String name, Environment environment, ChunkGenerator generator) {
          return WorldCreator.name(name).environment(environment).generator(generator).createWorld();
      }
  
      public World createWorld(String name, Environment environment, long seed, ChunkGenerator generator) {
          return WorldCreator.name(name).environment(environment).seed(seed).generator(generator).createWorld();
      }
  
      @Override
      public World createWorld(WorldCreator creator) {
          Validate.notNull(creator, "Creator may not be null");
  
          String name = creator.name();
          ChunkGenerator generator = creator.generator();
          File folder = new File(getWorldContainer(), name);
          World world = getWorld(name);
          WorldType type = WorldType.getType(creator.type().getName());
          boolean generateStructures = creator.generateStructures();
  
          if (world != null) {
              return world;
          }
  
          if ((folder.exists()) && (!folder.isDirectory())) {
              throw new IllegalArgumentException("File exists with the name '" + name + "' and isn't a folder");
          }
  
          if (generator == null) {
              generator = getGenerator(name);
          }
  
          Convertable converter = new WorldLoaderServer(getWorldContainer(), getHandle().getServer().dataConverterManager);
          if (converter.isConvertable(name)) {
              getLogger().info("Converting world '" + name + "'");
              converter.convert(name, new IProgressUpdate() {
                  private long b = System.currentTimeMillis();
  
                  public void a(String s) {}
  
                  public void a(int i) {
                      if (System.currentTimeMillis() - this.b >= 1000L) {
                          this.b = System.currentTimeMillis();
                          MinecraftServer.LOGGER.info("Converting... " + i + "%");
                      }
  
                  }
  
                  public void c(String s) {}
              });
          }
  
          int dimension = CraftWorld.CUSTOM_DIMENSION_OFFSET + console.worlds.size();
          boolean used = false;
          do {
              for (WorldServer server : console.worlds) {
                  used = server.dimension == dimension;
                  if (used) {
                      dimension++;
                      break;
                  }
              }
          } while(used);
          boolean hardcore = false;
  
          IDataManager sdm = new ServerNBTManager(getWorldContainer(), name, true, getHandle().getServer().dataConverterManager);
          WorldData worlddata = sdm.getWorldData();
          WorldSettings worldSettings = null;
          if (worlddata == null) {
              worldSettings = new WorldSettings(creator.seed(), EnumGamemode.getById(getDefaultGameMode().getValue()), generateStructures, hardcore, type);
              worldSettings.setGeneratorSettings(creator.generatorSettings());
              worlddata = new WorldData(worldSettings, name);
          }
          worlddata.checkName(name); // CraftBukkit - Migration did not rewrite the level.dat; This forces 1.8 to take the last loaded world as respawn (in this case the end)
          WorldServer internal = (WorldServer) new WorldServer(console, sdm, worlddata, dimension, console.methodProfiler, creator.environment(), generator).b();
  
          if (!(worlds.containsKey(name.toLowerCase(java.util.Locale.ENGLISH)))) {
              return null;
          }
  
          if (worldSettings != null) {
              internal.a(worldSettings);
          }
          internal.scoreboard = getScoreboardManager().getMainScoreboard().getHandle();
  
          internal.tracker = new EntityTracker(internal);
          internal.addIWorldAccess(new WorldManager(console, internal));
          internal.worldData.setDifficulty(EnumDifficulty.EASY);
          internal.setSpawnFlags(true, true);
          console.worlds.add(internal);
  
          pluginManager.callEvent(new WorldInitEvent(internal.getWorld()));
          System.out.println("Preparing start region for level " + (console.worlds.size() - 1) + " (Seed: " + internal.getSeed() + ")");
  
          if (internal.getWorld().getKeepSpawnInMemory()) {
              short short1 = internal.paperConfig.keepLoadedRange; // Paper
              long i = System.currentTimeMillis();
              for (int j = -short1; j <= short1; j += 16) {
                  for (int k = -short1; k <= short1; k += 16) {
                      long l = System.currentTimeMillis();
  
                      if (l < i) {
                          i = l;
                      }
  
                      if (l > i + 1000L) {
                          int i1 = (short1 * 2 + 1) * (short1 * 2 + 1);
                          int j1 = (j + short1) * (short1 * 2 + 1) + k + 1;
  
                          System.out.println("Preparing spawn area for " + name + ", " + (j1 * 100 / i1) + "%");
                          i = l;
                      }
  
                      BlockPosition chunkcoordinates = internal.getSpawn();
                      internal.getChunkProviderServer().getChunkAt(chunkcoordinates.getX() + j >> 4, chunkcoordinates.getZ() + k >> 4);
                  }
              }
          }
          pluginManager.callEvent(new WorldLoadEvent(internal.getWorld()));
          return internal.getWorld();
      }
  
      @Override
      public boolean unloadWorld(String name, boolean save) {
          return unloadWorld(getWorld(name), save);
      }
  
      @Override
      public boolean unloadWorld(World world, boolean save) {
          if (world == null) {
              return false;
          }
  
          WorldServer handle = ((CraftWorld) world).getHandle();
  
          if (!(console.worlds.contains(handle))) {
              return false;
          }
  
          if (handle.dimension == 0) {
              return false;
          }
  
          if (handle.players.size() > 0) {
              return false;
          }
  
          WorldUnloadEvent e = new WorldUnloadEvent(handle.getWorld());
          pluginManager.callEvent(e);
  
          if (e.isCancelled()) {
              return false;
          }
  
          if (save) {
              try {
                  handle.save(true, null);
                  handle.saveLevel();
              } catch (ExceptionWorldConflict ex) {
                  getLogger().log(Level.SEVERE, null, ex);
              }
          }
  
          worlds.remove(world.getName().toLowerCase(java.util.Locale.ENGLISH));
          console.worlds.remove(console.worlds.indexOf(handle));
  
          File parentFolder = world.getWorldFolder().getAbsoluteFile();
  
          // Synchronized because access to RegionFileCache.a is guarded by this lock.
          synchronized (RegionFileCache.class) {
              // RegionFileCache.a should be RegionFileCache.cache
              Iterator<Map.Entry<File, RegionFile>> i = RegionFileCache.a.entrySet().iterator();
              while(i.hasNext()) {
                  Map.Entry<File, RegionFile> entry = i.next();
                  File child = entry.getKey().getAbsoluteFile();
                  while (child != null) {
                      if (child.equals(parentFolder)) {
                          i.remove();
                          try {
                              entry.getValue().c(); // Should be RegionFile.close();
                          } catch (IOException ex) {
                              getLogger().log(Level.SEVERE, null, ex);
                          }
                          break;
                      }
                      child = child.getParentFile();
                  }
              }
          }
  
          return true;
      }
  
      public MinecraftServer getServer() {
          return console;
      }
  
      @Override
      public World getWorld(String name) {
          Validate.notNull(name, "Name cannot be null");
  
          return worlds.get(name.toLowerCase(java.util.Locale.ENGLISH));
      }
  
      @Override
      public World getWorld(UUID uid) {
          for (World world : worlds.values()) {
              if (world.getUID().equals(uid)) {
                  return world;
              }
          }
          return null;
      }
  
      public void addWorld(World world) {
          // Check if a World already exists with the UID.
          if (getWorld(world.getUID()) != null) {
              System.out.println("World " + world.getName() + " is a duplicate of another world and has been prevented from loading. Please delete the uid.dat file from " + world.getName() + "'s world directory if you want to be able to load the duplicate world.");
              return;
          }
          worlds.put(world.getName().toLowerCase(java.util.Locale.ENGLISH), world);
      }
  
      @Override
      public Logger getLogger() {
          return logger;
      }
  
      // Paper start - JLine update
      /*
      public ConsoleReader getReader() {
          return console.reader;
      }
      */
      // Paper end
  
      @Override
      public PluginCommand getPluginCommand(String name) {
          Command command = commandMap.getCommand(name);
  
          if (command instanceof PluginCommand) {
              return (PluginCommand) command;
          } else {
              return null;
          }
      }
  
      @Override
      public void savePlayers() {
          checkSaveState();
          playerList.savePlayers();
      }
  
      @Override
      public boolean addRecipe(Recipe recipe) {
          CraftRecipe toAdd;
          if (recipe instanceof CraftRecipe) {
              toAdd = (CraftRecipe) recipe;
          } else {
              if (recipe instanceof ShapedRecipe) {
                  toAdd = CraftShapedRecipe.fromBukkitRecipe((ShapedRecipe) recipe);
              } else if (recipe instanceof ShapelessRecipe) {
                  toAdd = CraftShapelessRecipe.fromBukkitRecipe((ShapelessRecipe) recipe);
              } else if (recipe instanceof FurnaceRecipe) {
                  toAdd = CraftFurnaceRecipe.fromBukkitRecipe((FurnaceRecipe) recipe);
              } else {
                  return false;
              }
          }
          toAdd.addToCraftingManager();
          return true;
      }
  
      @Override
      public List<Recipe> getRecipesFor(ItemStack result) {
          Validate.notNull(result, "Result cannot be null");
  
          List<Recipe> results = new ArrayList<Recipe>();
          Iterator<Recipe> iter = recipeIterator();
          while (iter.hasNext()) {
              Recipe recipe = iter.next();
              ItemStack stack = recipe.getResult();
              if (stack.getType() != result.getType()) {
                  continue;
              }
              if (result.getDurability() == -1 || result.getDurability() == stack.getDurability()) {
                  results.add(recipe);
              }
          }
          return results;
      }
  
      @Override
      public Iterator<Recipe> recipeIterator() {
          return new RecipeIterator();
      }
  
      @Override
      public void clearRecipes() {
          CraftingManager.recipes = new RegistryMaterials();
          RecipesFurnace.getInstance().recipes.clear();
          RecipesFurnace.getInstance().customRecipes.clear();
          RecipesFurnace.getInstance().customExperience.clear();
      }
  
      @Override
      public void resetRecipes() {
          CraftingManager.recipes = new RegistryMaterials();
          CraftingManager.init();
          RecipesFurnace.getInstance().recipes = new RecipesFurnace().recipes;
          RecipesFurnace.getInstance().customRecipes.clear();
          RecipesFurnace.getInstance().customExperience.clear();
      }
  
      @Override
      public Map<String, String[]> getCommandAliases() {
          ConfigurationSection section = commandsConfiguration.getConfigurationSection("aliases");
          Map<String, String[]> result = new LinkedHashMap<String, String[]>();
  
          if (section != null) {
              for (String key : section.getKeys(false)) {
                  List<String> commands;
  
                  if (section.isList(key)) {
                      commands = section.getStringList(key);
                  } else {
                      commands = ImmutableList.of(section.getString(key));
                  }
  
                  result.put(key, commands.toArray(new String[commands.size()]));
              }
          }
  
          return result;
      }
  
      public void removeBukkitSpawnRadius() {
          configuration.set("settings.spawn-radius", null);
          saveConfig();
      }
  
      public int getBukkitSpawnRadius() {
          return configuration.getInt("settings.spawn-radius", -1);
      }
  
      @Override
      public String getShutdownMessage() {
          return configuration.getString("settings.shutdown-message");
      }
  
      @Override
      public int getSpawnRadius() {
          return ((DedicatedServer) console).propertyManager.getInt("spawn-protection", 16);
      }
  
      @Override
      public void setSpawnRadius(int value) {
          configuration.set("settings.spawn-radius", value);
          saveConfig();
      }
  
      @Override
      public boolean getOnlineMode() {
          return online.value;
      }
  
      @Override
      public boolean getAllowFlight() {
          return console.getAllowFlight();
      }
  
      @Override
      public boolean isHardcore() {
          return console.isHardcore();
      }
  
      public ChunkGenerator getGenerator(String world) {
          ConfigurationSection section = configuration.getConfigurationSection("worlds");
          ChunkGenerator result = null;
  
          if (section != null) {
              section = section.getConfigurationSection(world);
  
              if (section != null) {
                  String name = section.getString("generator");
  
                  if ((name != null) && (!name.equals(""))) {
                      String[] split = name.split(":", 2);
                      String id = (split.length > 1) ? split[1] : null;
                      Plugin plugin = pluginManager.getPlugin(split[0]);
  
                      if (plugin == null) {
                          getLogger().severe("Could not set generator for default world '" + world + "': Plugin '" + split[0] + "' does not exist");
                      } else if (!plugin.isEnabled()) {
                          getLogger().severe("Could not set generator for default world '" + world + "': Plugin '" + plugin.getDescription().getFullName() + "' is not enabled yet (is it load:STARTUP?)");
                      } else {
                          try {
                              result = plugin.getDefaultWorldGenerator(world, id);
                              if (result == null) {
                                  getLogger().severe("Could not set generator for default world '" + world + "': Plugin '" + plugin.getDescription().getFullName() + "' lacks a default world generator");
                              }
                          } catch (Throwable t) {
                              plugin.getLogger().log(Level.SEVERE, "Could not set generator for default world '" + world + "': Plugin '" + plugin.getDescription().getFullName(), t);
                          }
                      }
                  }
              }
          }
  
          return result;
      }
  
      @Override
      @Deprecated
      public CraftMapView getMap(short id) {
          PersistentCollection collection = console.worlds.get(0).worldMaps;
          WorldMap worldmap = (WorldMap) collection.get(WorldMap.class, "map_" + id);
          if (worldmap == null) {
              return null;
          }
          return worldmap.mapView;
      }
  
      @Override
      public CraftMapView createMap(World world) {
          Validate.notNull(world, "World cannot be null");
  
          net.minecraft.server.ItemStack stack = new net.minecraft.server.ItemStack(Items.MAP, 1, -1);
          WorldMap worldmap = Items.FILLED_MAP.getSavedMap(stack, ((CraftWorld) world).getHandle());
          return worldmap.mapView;
      }
  
      @Override
      public void shutdown() {
          console.safeShutdown();
      }
  
      @Override
      public int broadcast(String message, String permission) {
          Set<CommandSender> recipients = new HashSet<>();
          for (Permissible permissible : getPluginManager().getPermissionSubscriptions(permission)) {
              if (permissible instanceof CommandSender && permissible.hasPermission(permission)) {
                  recipients.add((CommandSender) permissible);
              }
          }
  
          BroadcastMessageEvent broadcastMessageEvent = new BroadcastMessageEvent(message, recipients);
          getPluginManager().callEvent(broadcastMessageEvent);
  
          if (broadcastMessageEvent.isCancelled()) {
              return 0;
          }
  
          message = broadcastMessageEvent.getMessage();
  
          for (CommandSender recipient : recipients) {
              recipient.sendMessage(message);
          }
  
          return recipients.size();
      }
  
      // Paper start
      @Nullable
      public UUID getPlayerUniqueId(String name) {
          Player player = Bukkit.getPlayerExact(name);
          if (player != null) {
              return player.getUniqueId();
          }
          GameProfile profile;
          // Only fetch an online UUID in online mode
          if (MinecraftServer.getServer().getOnlineMode()
                  || (org.spigotmc.SpigotConfig.bungee && com.destroystokyo.paper.PaperConfig.bungeeOnlineMode)) {
              profile = console.getUserCache().getProfile( name );
          } else {
              // Make an OfflinePlayer using an offline mode UUID since the name has no profile
              profile = new GameProfile(UUID.nameUUIDFromBytes(("OfflinePlayer:" + name).getBytes(Charsets.UTF_8)), name);
          }
          return profile != null ? profile.getId() : null;
      }
      // Paper end
  
      @Override
      @Deprecated
      public OfflinePlayer getOfflinePlayer(String name) {
          Validate.notNull(name, "Name cannot be null");
          com.google.common.base.Preconditions.checkArgument( !org.apache.commons.lang.StringUtils.isBlank( name ), "Name cannot be blank" ); // Spigot
  
          OfflinePlayer result = getPlayerExact(name);
          if (result == null) {
              // Spigot Start
              GameProfile profile = null;
              // Only fetch an online UUID in online mode
              if ( MinecraftServer.getServer().getOnlineMode()
                      || (org.spigotmc.SpigotConfig.bungee && com.destroystokyo.paper.PaperConfig.bungeeOnlineMode)) // Paper - Handle via setting
              {
                  profile = console.getUserCache().getProfile( name );
              }
              // Spigot end
              if (profile == null) {
                  // Make an OfflinePlayer using an offline mode UUID since the name has no profile
                  result = getOfflinePlayer(new GameProfile(UUID.nameUUIDFromBytes(("OfflinePlayer:" + name).getBytes(Charsets.UTF_8)), name));
              } else {
                  // Use the GameProfile even when we get a UUID so we ensure we still have a name
                  result = getOfflinePlayer(profile);
              }
          } else {
              offlinePlayers.invalidate(result.getUniqueId()); // Akarin - caffeine
          }
  
          return result;
      }
  
      @Override
      public OfflinePlayer getOfflinePlayer(UUID id) {
          Validate.notNull(id, "UUID cannot be null");
  
          OfflinePlayer result = getPlayer(id);
          if (result == null) {
              result = offlinePlayers.getIfPresent(id); // Akarin - caffeine
              if (result == null) {
                  result = new CraftOfflinePlayer(this, new GameProfile(id, null));
                  offlinePlayers.put(id, result);
              }
          } else {
              offlinePlayers.invalidate(id); // Akarin - caffeine
          }
  
          return result;
      }
  
      public OfflinePlayer getOfflinePlayer(GameProfile profile) {
          OfflinePlayer player = new CraftOfflinePlayer(this, profile);
          offlinePlayers.put(profile.getId(), player);
          return player;
      }
  
      @Override
      @SuppressWarnings("unchecked")
      public Set<String> getIPBans() {
          return new HashSet<String>(Arrays.asList(playerList.getIPBans().getEntries()));
      }
  
      @Override
      public void banIP(String address) {
          Validate.notNull(address, "Address cannot be null.");
  
          this.getBanList(org.bukkit.BanList.Type.IP).addBan(address, null, null, null);
      }
  
      @Override
      public void unbanIP(String address) {
          Validate.notNull(address, "Address cannot be null.");
  
          this.getBanList(org.bukkit.BanList.Type.IP).pardon(address);
      }
  
      @Override
      public Set<OfflinePlayer> getBannedPlayers() {
          Set<OfflinePlayer> result = new HashSet<OfflinePlayer>();
  
          for (JsonListEntry entry : playerList.getProfileBans().getValues()) {
              result.add(getOfflinePlayer((GameProfile) entry.getKey()));
          }        
  
          return result;
      }
  
      @Override
      public BanList getBanList(BanList.Type type) {
          Validate.notNull(type, "Type cannot be null");
  
          switch(type){
          case IP:
              return new CraftIpBanList(playerList.getIPBans());
          case NAME:
          default:
              return new CraftProfileBanList(playerList.getProfileBans());
          }
      }
  
      @Override
      public void setWhitelist(boolean value) {
          playerList.setHasWhitelist(value);
          console.getPropertyManager().setProperty("white-list", value);
      }
  
      @Override
      public Set<OfflinePlayer> getWhitelistedPlayers() {
          Set<OfflinePlayer> result = new LinkedHashSet<OfflinePlayer>();
  
          for (JsonListEntry entry : playerList.getWhitelist().getValues()) {
              result.add(getOfflinePlayer((GameProfile) entry.getKey()));
          }
  
          return result;
      }
  
      @Override
      public Set<OfflinePlayer> getOperators() {
          Set<OfflinePlayer> result = new HashSet<OfflinePlayer>();
  
          for (JsonListEntry entry : playerList.getOPs().getValues()) {
              result.add(getOfflinePlayer((GameProfile) entry.getKey()));
          }
  
          return result;
      }
  
      @Override
      public void reloadWhitelist() {
          playerList.reloadWhitelist();
      }
  
      @Override
      public GameMode getDefaultGameMode() {
          return GameMode.getByValue(console.worlds.get(0).getWorldData().getGameType().getId());
      }
  
      @Override
      public void setDefaultGameMode(GameMode mode) {
          Validate.notNull(mode, "Mode cannot be null");
  
          for (World world : getWorlds()) {
              ((CraftWorld) world).getHandle().worldData.setGameType(EnumGamemode.getById(mode.getValue()));
          }
      }
  
      @Override
      public ConsoleCommandSender getConsoleSender() {
          return console.console;
      }
  
      public EntityMetadataStore getEntityMetadata() {
          return entityMetadata;
      }
  
      public PlayerMetadataStore getPlayerMetadata() {
          return playerMetadata;
      }
  
      public WorldMetadataStore getWorldMetadata() {
          return worldMetadata;
      }
  
      @Override
      public File getWorldContainer() {
          if (this.getServer().universe != null) {
              return this.getServer().universe;
          }
  
          if (container == null) {
              container = new File(configuration.getString("settings.world-container", "."));
          }
  
          return container;
      }
  
      @Override
      public OfflinePlayer[] getOfflinePlayers() {
          WorldNBTStorage storage = (WorldNBTStorage) console.worlds.get(0).getDataManager();
          String[] files = storage.getPlayerDir().list(new DatFileFilter());
          Set<OfflinePlayer> players = new HashSet<OfflinePlayer>();
  
          for (String file : files) {
              try {
                  players.add(getOfflinePlayer(UUID.fromString(file.substring(0, file.length() - 4))));
              } catch (IllegalArgumentException ex) {
                  // Who knows what is in this directory, just ignore invalid files
              }
          }
  
          players.addAll(getOnlinePlayers());
  
          return players.toArray(new OfflinePlayer[players.size()]);
      }
  
      @Override
      public Messenger getMessenger() {
          return messenger;
      }
  
      @Override
      public void sendPluginMessage(Plugin source, String channel, byte[] message) {
          StandardMessenger.validatePluginMessage(getMessenger(), source, channel, message);
  
          for (Player player : getOnlinePlayers()) {
              player.sendPluginMessage(source, channel, message);
          }
      }
  
      @Override
      public Set<String> getListeningPluginChannels() {
          Set<String> result = new HashSet<String>();
  
          for (Player player : getOnlinePlayers()) {
              result.addAll(player.getListeningPluginChannels());
          }
  
          return result;
      }
  
      @Override
      public Inventory createInventory(InventoryHolder owner, InventoryType type) {
          // TODO: Create the appropriate type, rather than Custom?
          return new CraftInventoryCustom(owner, type);
      }
  
      @Override
      public Inventory createInventory(InventoryHolder owner, InventoryType type, String title) {
          return new CraftInventoryCustom(owner, type, title);
      }
  
      @Override
      public Inventory createInventory(InventoryHolder owner, int size) throws IllegalArgumentException {
          Validate.isTrue(size % 9 == 0, "Chests must have a size that is a multiple of 9!");
          return new CraftInventoryCustom(owner, size);
      }
  
      @Override
      public Inventory createInventory(InventoryHolder owner, int size, String title) throws IllegalArgumentException {
          Validate.isTrue(size % 9 == 0, "Chests must have a size that is a multiple of 9!");
          return new CraftInventoryCustom(owner, size, title);
      }
  
      @Override
      public Merchant createMerchant(String title) {
          return new CraftMerchantCustom(title == null ? InventoryType.MERCHANT.getDefaultTitle() : title);
      }
  
      @Override
      public HelpMap getHelpMap() {
          return helpMap;
      }
  
      @Override // Paper - add override
      public SimpleCommandMap getCommandMap() {
          return commandMap;
      }
  
      @Override
      public int getMonsterSpawnLimit() {
          return monsterSpawn;
      }
  
      @Override
      public int getAnimalSpawnLimit() {
          return animalSpawn;
      }
  
      @Override
      public int getWaterAnimalSpawnLimit() {
          return waterAnimalSpawn;
      }
  
      @Override
      public int getAmbientSpawnLimit() {
          return ambientSpawn;
      }
  
      @Override
      public boolean isPrimaryThread() {
          return Thread.currentThread().equals(console.primaryThread);
      }
  
      @Override
      public String getMotd() {
          return console.getMotd();
      }
  
      @Override
      public WarningState getWarningState() {
          return warningState;
      }
  
      public List<String> tabComplete(net.minecraft.server.ICommandListener sender, String message, BlockPosition pos, boolean forceCommand) {
          if (!(sender instanceof EntityPlayer)) {
              return ImmutableList.of();
          }
  
          List<String> offers;
          Player player = ((EntityPlayer) sender).getBukkitEntity();
          if (message.startsWith("/") || forceCommand) {
              offers = tabCompleteCommand(player, message, pos);
          } else {
              offers = tabCompleteChat(player, message);
          }
          
          TabCompleteEvent tabEvent = new TabCompleteEvent(player, message, offers, message.startsWith("/") || forceCommand, pos != null ? MCUtil.toLocation(((CraftWorld) player.getWorld()).getHandle(), pos) : null); // Paper
          getPluginManager().callEvent(tabEvent);
  
          return tabEvent.isCancelled() ? Collections.EMPTY_LIST : tabEvent.getCompletions();
      }
  
      public List<String> tabCompleteCommand(Player player, String message, BlockPosition pos) {
          // Spigot Start
  		if ( (org.spigotmc.SpigotConfig.tabComplete < 0 || message.length() <= org.spigotmc.SpigotConfig.tabComplete) && !message.contains( " " ) )
          {
              return ImmutableList.of();
          }
          // Spigot End
  
          List<String> completions = null;
          try {
              if (message.startsWith("/")) {
                  // Trim leading '/' if present (won't always be present in command blocks)
                  message = message.substring(1);
              }
              if (pos == null) {
                  completions = getCommandMap().tabComplete(player, message);
              } else {
                  completions = getCommandMap().tabComplete(player, message, new Location(player.getWorld(), pos.getX(), pos.getY(), pos.getZ()));
              }
          } catch (CommandException ex) {
              player.sendMessage(ChatColor.RED + "An internal error occurred while attempting to tab-complete this command");
              getLogger().log(Level.SEVERE, "Exception when " + player.getName() + " attempted to tab complete " + message, ex);
          }
  
          return completions == null ? ImmutableList.<String>of() : completions;
      }
  
      public List<String> tabCompleteChat(Player player, String message) {
          List<String> completions = new ArrayList<String>();
          PlayerChatTabCompleteEvent event = new PlayerChatTabCompleteEvent(player, message, completions);
          String token = event.getLastToken();
          for (Player p : getOnlinePlayers()) {
              if (player.canSee(p) && StringUtil.startsWithIgnoreCase(p.getName(), token)) {
                  completions.add(p.getName());
              }
          }
          pluginManager.callEvent(event);
  
          Iterator<?> it = completions.iterator();
          while (it.hasNext()) {
              Object current = it.next();
              if (!(current instanceof String)) {
                  // Sanity
                  it.remove();
              }
          }
          Collections.sort(completions, String.CASE_INSENSITIVE_ORDER);
          return completions;
      }
  
      @Override
      public CraftItemFactory getItemFactory() {
          return CraftItemFactory.instance();
      }
  
      @Override
      public CraftScoreboardManager getScoreboardManager() {
          return scoreboardManager;
      }
  
      public void checkSaveState() {
          if (this.playerCommandState || this.printSaveWarning || this.console.autosavePeriod <= 0) {
              return;
          }
          this.printSaveWarning = true;
          getLogger().log(Level.WARNING, "A manual (plugin-induced) save has been detected while server is configured to auto-save. This may affect performance.", warningState == WarningState.ON ? new Throwable() : null);
      }
  
      @Override
      public CraftIconCache getServerIcon() {
          return icon;
      }
  
      @Override
      public CraftIconCache loadServerIcon(File file) throws Exception {
          Validate.notNull(file, "File cannot be null");
          if (!file.isFile()) {
              throw new IllegalArgumentException(file + " is not a file");
          }
          return loadServerIcon0(file);
      }
  
      static CraftIconCache loadServerIcon0(File file) throws Exception {
          return loadServerIcon0(ImageIO.read(file));
      }
  
      @Override
      public CraftIconCache loadServerIcon(BufferedImage image) throws Exception {
          Validate.notNull(image, "Image cannot be null");
          return loadServerIcon0(image);
      }
  
      static CraftIconCache loadServerIcon0(BufferedImage image) throws Exception {
          ByteBuf bytebuf = Unpooled.buffer();
  
          Validate.isTrue(image.getWidth() == 64, "Must be 64 pixels wide");
          Validate.isTrue(image.getHeight() == 64, "Must be 64 pixels high");
          ImageIO.write(image, "PNG", new ByteBufOutputStream(bytebuf));
          ByteBuf bytebuf1 = Base64.encode(bytebuf);
  
          return new CraftIconCache("data:image/png;base64," + bytebuf1.toString(Charsets.UTF_8).replace("\n", "")); // Paper - Fix encoding for 1.13+ clients, still compat w/ 1.12 clients
      }
  
      @Override
      public void setIdleTimeout(int threshold) {
          console.setIdleTimeout(threshold);
      }
  
      @Override
      public int getIdleTimeout() {
          return console.getIdleTimeout();
      }
  
      @Override
      public ChunkGenerator.ChunkData createChunkData(World world) {
          return new CraftChunkData(world);
      }
  
      @Override
      public BossBar createBossBar(String title, BarColor color, BarStyle style, BarFlag... flags) {
          return new CraftBossBar(title, color, style, flags);
      }
  
      @Override
      public Entity getEntity(UUID uuid) {
          Validate.notNull(uuid, "UUID cannot be null");
          net.minecraft.server.Entity entity = console.a(uuid); // PAIL: getEntity
          return entity == null ? null : entity.getBukkitEntity();
      }
  
      @Override
      public org.bukkit.advancement.Advancement getAdvancement(NamespacedKey key) {
          Preconditions.checkArgument(key != null, "key");
  
          Advancement advancement = console.getAdvancementData().a(CraftNamespacedKey.toMinecraft(key));
          return (advancement == null) ? null : advancement.bukkit;
      }
  
      @Override
      public Iterator<org.bukkit.advancement.Advancement> advancementIterator() {
          return Iterators.unmodifiableIterator(Iterators.transform(console.getAdvancementData().c().iterator(), new Function<Advancement, org.bukkit.advancement.Advancement>() { // PAIL: rename
              @Override
              public org.bukkit.advancement.Advancement apply(Advancement advancement) {
                  return advancement.bukkit;
              }
          }));
      }
  
      @Deprecated
      @Override
      public UnsafeValues getUnsafe() {
          return CraftMagicNumbers.INSTANCE;
      }
  
      // Paper - Add getTPS API - Further improve tick loop
      @Override
      public double[] getTPS() {
          return new double[] {
                  MinecraftServer.getServer().tps1.getAverage(),
                  MinecraftServer.getServer().tps5.getAverage(),
                  MinecraftServer.getServer().tps15.getAverage()
          };
      }
      // Paper end
  
      private final Spigot spigot = new Spigot()
      {
  
          @Deprecated
          @Override
          public YamlConfiguration getConfig()
          {
              return org.spigotmc.SpigotConfig.config;
          }
  
          @Override
          public YamlConfiguration getBukkitConfig()
          {
              return configuration;
          }
  
          @Override
          public YamlConfiguration getSpigotConfig()
          {
              return org.spigotmc.SpigotConfig.config;
          }
  
          @Override
          public YamlConfiguration getPaperConfig()
          {
              return com.destroystokyo.paper.PaperConfig.config;
          }
  
          @Override
          public void restart() {
              org.spigotmc.RestartCommand.restart();
          }
  
          @Override
          public void broadcast(BaseComponent component) {
              for (Player player : getOnlinePlayers()) {
                  player.spigot().sendMessage(component);
              }
          }
  
          @Override
          public void broadcast(BaseComponent... components) {
              for (Player player : getOnlinePlayers()) {
                  player.spigot().sendMessage(components);
              }
          }
      };
  
      public Spigot spigot()
      {
          return spigot;
      }
  
      // Paper start
      @SuppressWarnings({"rawtypes", "unchecked"})
      public static boolean dumpHeap(File file) {
          try {
              if (file.getParentFile() != null) {
                  file.getParentFile().mkdirs();
              }
  
              Class clazz = Class.forName("com.sun.management.HotSpotDiagnosticMXBean");
              javax.management.MBeanServer server = java.lang.management.ManagementFactory.getPlatformMBeanServer();
              Object hotspotMBean = java.lang.management.ManagementFactory.newPlatformMXBeanProxy(server, "com.sun.management:type=HotSpotDiagnostic", clazz);
              java.lang.reflect.Method m = clazz.getMethod("dumpHeap", String.class, boolean.class);
              m.invoke(hotspotMBean, file.getPath(), true);
              return true;
          } catch (Throwable t) {
              Bukkit.getLogger().severe("Could not write heap to " + file);
              t.printStackTrace();
              return false;
          }
      }
  
      @Override
      public void reloadPermissions() {
          pluginManager.clearPermissions();
          if (com.destroystokyo.paper.PaperConfig.loadPermsBeforePlugins) loadCustomPermissions();
          for (Plugin plugin : pluginManager.getPlugins()) {
              for (Permission perm : plugin.getDescription().getPermissions()) {
                  try {
                      pluginManager.addPermission(perm);
                  } catch (IllegalArgumentException ex) {
                      getLogger().log(Level.WARNING, "Plugin " + plugin.getDescription().getFullName() + " tried to register permission '" + perm.getName() + "' but it's already registered", ex);
                  }
              }
          }
          if (!com.destroystokyo.paper.PaperConfig.loadPermsBeforePlugins) loadCustomPermissions();
          DefaultPermissions.registerCorePermissions();
          CraftDefaultPermissions.registerCorePermissions();
      }
  
      @Override
      public boolean reloadCommandAliases() {
          Set<String> removals = getCommandAliases().keySet().stream()
                  .map(key -> key.toLowerCase(java.util.Locale.ENGLISH))
                  .collect(java.util.stream.Collectors.toSet());
          getCommandMap().getKnownCommands().keySet().removeIf(removals::contains);
          File file = getCommandsConfigFile();
          try {
              commandsConfiguration.load(file);
          } catch (FileNotFoundException ex) {
              return false;
          } catch (IOException | org.bukkit.configuration.InvalidConfigurationException ex) {
              Bukkit.getLogger().log(Level.SEVERE, "Cannot load " + file, ex);
              return false;
          }
          commandMap.registerServerAliases();
          return true;
      }
  
      @Override
      public boolean suggestPlayerNamesWhenNullTabCompletions() {
          return com.destroystokyo.paper.PaperConfig.suggestPlayersWhenNullTabCompletions;
      }
  
      public com.destroystokyo.paper.profile.PlayerProfile createProfile(@Nonnull UUID uuid) {
          return createProfile(uuid, null);
      }
  
      public com.destroystokyo.paper.profile.PlayerProfile createProfile(@Nonnull String name) {
          return createProfile(null, name);
      }
  
      public com.destroystokyo.paper.profile.PlayerProfile createProfile(@Nullable UUID uuid, @Nullable String name) {
          Player player = uuid != null ? Bukkit.getPlayer(uuid) : (name != null ? Bukkit.getPlayerExact(name) : null);
          if (player != null) {
              return new com.destroystokyo.paper.profile.CraftPlayerProfile((CraftPlayer)player);
          }
          return new com.destroystokyo.paper.profile.CraftPlayerProfile(uuid, name);
      }
      // Paper end
  }
  