me.ryanhamshire.PopulationDensity.PopulationDensity.java Source code

Java tutorial

Introduction

Here is the source code for me.ryanhamshire.PopulationDensity.PopulationDensity.java

Source

/*
PopulationDensity Server Plugin for Minecraft
Copyright (C) 2012 Ryan Hamshire
    
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
    
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.
    
You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package me.ryanhamshire.PopulationDensity;

import org.apache.commons.lang.WordUtils;
import org.bstats.bukkit.Metrics;
import org.bukkit.ChatColor;
import org.bukkit.Chunk;
import org.bukkit.ChunkSnapshot;
import org.bukkit.GameMode;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.OfflinePlayer;
import org.bukkit.Sound;
import org.bukkit.World;
import org.bukkit.World.Environment;
import org.bukkit.WorldBorder;
import org.bukkit.block.Block;
import org.bukkit.block.BlockFace;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.PluginCommand;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Monster;
import org.bukkit.entity.Player;
import org.bukkit.plugin.PluginManager;
import org.bukkit.plugin.java.JavaPlugin;
import org.bukkit.util.Vector;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.logging.Logger;

public class PopulationDensity extends JavaPlugin {
    //for convenience, a reference to the instance of this plugin
    public static PopulationDensity instance;

    DropShipTeleporter dropShipTeleporterInstance;

    //for logging to the console and log file
    //Um wat why are we using the Minecraft logger... Oh shoot static is everywhere here :(
    //private static Logger log = Logger.getLogger("Minecraft");
    private static Logger log;

    //developer configuration, not modifiable by users
    public static final int REGION_SIZE = 400;

    //the world managed by this plugin
    public static World ManagedWorld;

    //the default world, not managed by this plugin
    //(may be null in some configurations)
    public static World CityWorld;

    //this handles data storage, like player and region data
    public DataStore dataStore;

    //tracks server perforamnce
    static float serverTicksPerSecond = 20;
    static int minutesLagging = 0;

    //lag-reducing measures
    static boolean grindersStopped = false;
    static boolean bootingIdlePlayersForLag = false;

    //user configuration, loaded/saved from a config.yml
    public boolean allowTeleportation;
    public boolean teleportFromAnywhere;
    public boolean newPlayersSpawnInHomeRegion;
    public boolean respawnInHomeRegion;
    public String cityWorldName;
    public String managedWorldName;
    public int maxDistanceFromSpawnToUseHomeRegion;
    public double densityRatio;
    public int maxIdleMinutes;
    public boolean enableLoginQueue;
    public int reservedSlotsForAdmins;
    public String queueMessage;
    public int hoursBetweenScans;
    public boolean buildRegionPosts;
    public boolean newestRegionRequiresPermission;
    public boolean regrowGrass;
    public boolean respawnAnimals;
    public boolean regrowTrees;
    public boolean thinAnimalAndMonsterCrowds;
    public boolean thinIronGolemsToo;
    public boolean preciseWorldSpawn;
    public int woodMinimum;
    public int resourceMinimum;
    public Material postMaterialTop = Material.GLOWSTONE;
    public Material postMaterialMidTop = Material.GLOWSTONE;
    public Material postMaterialMidBottom = Material.GLOWSTONE;
    public Material postMaterialBottom = Material.GLOWSTONE;
    public Material outerPlatformMaterial = Material.STONE_BRICKS;
    public Material innerPlatformMaterial = Material.STONE_BRICKS;
    public int nearbyMonsterSpawnLimit;
    public int maxRegionNameLength = 10;
    public boolean abandonedFarmAnimalsDie;
    public boolean unusedMinecartsVanish;
    public boolean markRemovedEntityLocations;
    public boolean removeWildSkeletalHorses;

    public boolean config_captureSpigotTimingsWhenLagging;
    public boolean config_bootIdlePlayersWhenLagging;
    public boolean config_disableGrindersWhenLagging;
    public int config_maximumHoppersPerChunk;
    public boolean config_keepSpawnRegionPostLoaded;
    public boolean config_keepAllRegionPostsLoaded;
    boolean config_launchAndDropPlayers;
    boolean config_launchAndDropNewPlayers;
    boolean config_teleportAnimals = true;

    public int minimumRegionPostY;

    public String[] topSignContent;
    public String[] sideSignContent;
    public String[] instructionsSignContent;
    public String[] mainCustomSignContent;
    public String[] northCustomSignContent;
    public String[] southCustomSignContent;
    public String[] eastCustomSignContent;
    public String[] westCustomSignContent;

    public int postProtectionRadius;

    boolean isSpigotServer = false;

    List<String> config_regionNames;

    public synchronized static void AddLogEntry(String entry) {
        log.info(entry);
    }

    //initializes well...   everything
    public void onEnable() {
        instance = this;
        log = instance.getLogger();

        try {
            Class.forName("org.spigotmc.SpigotConfig");
            isSpigotServer = true;
        } catch (ClassNotFoundException e) {
        }

        //load the config if it exists
        FileConfiguration config = YamlConfiguration.loadConfiguration(new File(DataStore.configFilePath));

        //prepare default setting for managed world...
        String defaultManagedWorldName = "";

        //build a list of normal environment worlds
        List<World> worlds = this.getServer().getWorlds();
        ArrayList<World> normalWorlds = new ArrayList<World>();
        for (int i = 0; i < worlds.size(); i++) {
            if (worlds.get(i).getEnvironment() == Environment.NORMAL) {
                normalWorlds.add(worlds.get(i));
            }
        }

        //if there's only one, make it the default
        if (normalWorlds.size() == 1) {
            defaultManagedWorldName = normalWorlds.get(0).getName();
        }

        //read configuration settings (note defaults)
        this.allowTeleportation = config.getBoolean("PopulationDensity.AllowTeleportation", true);
        this.teleportFromAnywhere = config.getBoolean("PopulationDensity.TeleportFromAnywhere", false);
        this.newPlayersSpawnInHomeRegion = config.getBoolean("PopulationDensity.NewPlayersSpawnInHomeRegion", true);
        this.respawnInHomeRegion = config.getBoolean("PopulationDensity.RespawnInHomeRegion", true);
        this.cityWorldName = config.getString("PopulationDensity.CityWorldName", "");
        this.maxDistanceFromSpawnToUseHomeRegion = config
                .getInt("PopulationDensity.MaxDistanceFromSpawnToUseHomeRegion", 25);
        this.managedWorldName = config.getString("PopulationDensity.ManagedWorldName", defaultManagedWorldName);
        this.densityRatio = config.getDouble("PopulationDensity.DensityRatio", 1.0);
        this.maxIdleMinutes = config.getInt("PopulationDensity.MaxIdleMinutes", 10);
        this.enableLoginQueue = config.getBoolean("PopulationDensity.LoginQueueEnabled", true);
        this.reservedSlotsForAdmins = config.getInt("PopulationDensity.ReservedSlotsForAdministrators", 1);
        if (this.reservedSlotsForAdmins < 0)
            this.reservedSlotsForAdmins = 0;
        this.queueMessage = config.getString("PopulationDensity.LoginQueueMessage",
                "%queuePosition% of %queueLength% in queue.  Reconnect within 3 minutes to keep your place.  :)");
        this.hoursBetweenScans = config.getInt("PopulationDensity.HoursBetweenScans", 6);
        this.buildRegionPosts = config.getBoolean("PopulationDensity.BuildRegionPosts", true);
        this.newestRegionRequiresPermission = config.getBoolean("PopulationDensity.NewestRegionRequiresPermission",
                false);
        this.regrowGrass = config.getBoolean("PopulationDensity.GrassRegrows", true);
        this.respawnAnimals = config.getBoolean("PopulationDensity.AnimalsRespawn", true);
        this.regrowTrees = config.getBoolean("PopulationDensity.TreesRegrow", true);
        this.config_maximumHoppersPerChunk = config.getInt("PopulationDensity.Maximum Hoppers Per Chunk", 10);
        this.thinAnimalAndMonsterCrowds = config.getBoolean("PopulationDensity.ThinOvercrowdedAnimalsAndMonsters",
                true);
        this.thinIronGolemsToo = config.getBoolean("PopulationDensity.ThinOverCrowdedIronGolems",
                this.thinIronGolemsToo);
        this.minimumRegionPostY = config.getInt("PopulationDensity.MinimumRegionPostY", 62);
        this.preciseWorldSpawn = config.getBoolean("PopulationDensity.PreciseWorldSpawn", false);
        this.woodMinimum = config.getInt("PopulationDensity.MinimumWoodAvailableToPlaceNewPlayers", 200);
        this.resourceMinimum = config.getInt("PopulationDensity.MinimumResourceScoreToPlaceNewPlayers", 200);
        this.postProtectionRadius = config.getInt("PopulationDensity.PostProtectionDistance", 2);
        this.maxRegionNameLength = config.getInt("PopulationDensity.Maximum Region Name Length", 10);

        this.config_disableGrindersWhenLagging = config
                .getBoolean("PopulationDensity.Disable Monster Grinders When Lagging", true);
        this.config_bootIdlePlayersWhenLagging = config
                .getBoolean("PopulationDensity.Boot Idle Players When Lagging", true);
        this.config_captureSpigotTimingsWhenLagging = config
                .getBoolean("PopulationDensity.Capture Spigot Timings When Lagging", false);
        this.config_keepSpawnRegionPostLoaded = config.getBoolean("PopulationDensity.KeepSpawnRegionPostLoaded",
                true);
        this.config_keepAllRegionPostsLoaded = config.getBoolean("PopulationDensity.KeepAllRegionPostsLoaded",
                false);
        this.config_launchAndDropPlayers = config.getBoolean("PopulationDensity.LaunchAndDropPlayers", true);
        this.config_launchAndDropNewPlayers = config.getBoolean("PopulationDensity.LaunchAndDropNewPlayers",
                config_launchAndDropPlayers);
        this.config_teleportAnimals = config.getBoolean("PopulationDensity.TeleportAnimals",
                config_teleportAnimals);

        String top = config.getString("PopulationDensity.PostDesign.TopBlock", "GLOWSTONE");
        String midTop = config.getString("PopulationDensity.PostDesign.MidTopBlock", "GLOWSTONE");
        String midBottom = config.getString("PopulationDensity.PostDesign.MidBottomBlock", "GLOWSTONE");
        String bottom = config.getString("PopulationDensity.PostDesign.BottomBlock", "GLOWSTONE");
        String outerPlat = config.getString("PopulationDensity.PostDesign.PlatformOuterRing", "STONE_BRICKS");
        String innerPlat = config.getString("PopulationDensity.PostDesign.PlatformInnerRing", "STONE_BRICKS");
        this.nearbyMonsterSpawnLimit = config.getInt("PopulationDensity.Max Monsters In Chunk To Spawn More", 2);
        this.nearbyMonsterSpawnLimit = config.getInt("PopulationDensity.Max Monsters Nearby For More To Spawn",
                nearbyMonsterSpawnLimit);
        this.abandonedFarmAnimalsDie = config.getBoolean("PopulationDensity.Abandoned Farm Animals Die", true);
        this.unusedMinecartsVanish = config.getBoolean("PopulationDensity.Unused Minecarts Vanish", true);
        this.markRemovedEntityLocations = config
                .getBoolean("PopulationDensity.MarkRemovedAnimalLocationsWithShrubs", true);
        this.removeWildSkeletalHorses = config.getBoolean("PopulationDensity.Remove Wild Skeletal Horses", true);

        Material resultT = this.processMaterials(top);
        if (resultT != null) {
            this.postMaterialTop = resultT;
        }

        Material resultMT = this.processMaterials(midTop);
        if (resultMT != null) {
            this.postMaterialMidTop = resultMT;
        }

        Material resultMB = this.processMaterials(midBottom);
        if (resultMB != null) {
            this.postMaterialMidBottom = resultMB;
        }

        Material resultB = this.processMaterials(bottom);
        if (resultB != null) {
            this.postMaterialBottom = resultB;
        }

        Material resultO = this.processMaterials(outerPlat);
        if (resultO != null) {
            this.outerPlatformMaterial = resultO;
        }

        Material resultI = this.processMaterials(innerPlat);
        if (resultI != null) {
            this.innerPlatformMaterial = resultI;
        }

        List<String> defaultRegionNames = Arrays.asList("redstone", "dew", "creeper", "sword", "wintersebb",
                "fjord", "vista", "breeze", "tide", "stream", "glenwood", "journey", "cragstone", "pickaxe", "axe",
                "hammer", "anvil", "mist", "sunrise", "sunset", "copper", "coal", "shovel", "minecart", "railway",
                "dig", "chasm", "basalt", "agate", "boat", "grass", "gust", "ruby", "emerald", "stone", "peak",
                "ore", "boulder", "hilltop", "horizon", "fog", "cloud", "canopy", "gravel", "torch", "obsidian",
                "treetop", "storm", "gold", "canopy", "leaf", "summit", "glade", "trail", "seed", "diamond",
                "armor", "sand", "flint", "field", "steel", "helm", "gorge", "campfire", "workshop", "rubble",
                "iron", "chisel", "moon", "shrub", "zombie", "stem", "vale", "pumpkin", "lantern", "copper",
                "moonBeam", "soil", "dust");

        this.config_regionNames = new ArrayList<String>();
        List<String> regionNames = config.getStringList("PopulationDensity.Region Name List");
        if (regionNames == null || regionNames.size() == 0) {
            regionNames = defaultRegionNames;
        }

        for (String regionName : regionNames) {
            String error = getRegionNameError(regionName, true);
            if (error != null) {
                AddLogEntry("Unable to use region name + '" + regionName + "':" + error);
            } else {
                this.config_regionNames.add(regionName);
            }
        }

        //and write those values back and save. this ensures the config file is available on disk for editing
        FileConfiguration outConfig = new YamlConfiguration();
        outConfig.set("PopulationDensity.NewPlayersSpawnInHomeRegion", this.newPlayersSpawnInHomeRegion);
        outConfig.set("PopulationDensity.RespawnInHomeRegion", this.respawnInHomeRegion);
        outConfig.set("PopulationDensity.CityWorldName", this.cityWorldName);
        outConfig.set("PopulationDensity.AllowTeleportation", this.allowTeleportation);
        outConfig.set("PopulationDensity.TeleportFromAnywhere", this.teleportFromAnywhere);
        outConfig.set("PopulationDensity.MaxDistanceFromSpawnToUseHomeRegion",
                this.maxDistanceFromSpawnToUseHomeRegion);
        outConfig.set("PopulationDensity.ManagedWorldName", this.managedWorldName);
        outConfig.set("PopulationDensity.DensityRatio", this.densityRatio);
        outConfig.set("PopulationDensity.MaxIdleMinutes", this.maxIdleMinutes);
        outConfig.set("PopulationDensity.LoginQueueEnabled", this.enableLoginQueue);
        outConfig.set("PopulationDensity.ReservedSlotsForAdministrators", this.reservedSlotsForAdmins);
        outConfig.set("PopulationDensity.LoginQueueMessage", this.queueMessage);
        outConfig.set("PopulationDensity.HoursBetweenScans", this.hoursBetweenScans);
        outConfig.set("PopulationDensity.BuildRegionPosts", this.buildRegionPosts);
        outConfig.set("PopulationDensity.NewestRegionRequiresPermission", this.newestRegionRequiresPermission);
        outConfig.set("PopulationDensity.GrassRegrows", this.regrowGrass);
        outConfig.set("PopulationDensity.AnimalsRespawn", this.respawnAnimals);
        outConfig.set("PopulationDensity.TreesRegrow", this.regrowTrees);
        outConfig.set("PopulationDensity.Max Monsters Nearby For More To Spawn", this.nearbyMonsterSpawnLimit);
        outConfig.set("PopulationDensity.ThinOvercrowdedAnimalsAndMonsters", this.thinAnimalAndMonsterCrowds);
        outConfig.set("PopulationDensity.ThinOvercrowdedIronGolems", this.thinIronGolemsToo);
        outConfig.set("PopulationDensity.Abandoned Farm Animals Die", this.abandonedFarmAnimalsDie);
        outConfig.set("PopulationDensity.Unused Minecarts Vanish", this.unusedMinecartsVanish);
        outConfig.set("PopulationDensity.MarkRemovedAnimalLocationsWithShrubs", this.markRemovedEntityLocations);
        outConfig.set("PopulationDensity.Remove Wild Skeletal Horses", this.removeWildSkeletalHorses);
        outConfig.set("PopulationDensity.Disable Monster Grinders When Lagging",
                this.config_disableGrindersWhenLagging);
        outConfig.set("PopulationDensity.Maximum Hoppers Per Chunk", this.config_maximumHoppersPerChunk);
        outConfig.set("PopulationDensity.Boot Idle Players When Lagging", this.config_bootIdlePlayersWhenLagging);
        outConfig.set("PopulationDensity.Capture Spigot Timings When Lagging",
                this.config_captureSpigotTimingsWhenLagging);
        outConfig.set("PopulationDensity.KeepSpawnRegionPostLoaded", this.config_keepSpawnRegionPostLoaded);
        outConfig.set("PopulationDensity.KeepAllRegionPostsLoaded", this.config_keepAllRegionPostsLoaded);
        outConfig.set("PopulationDensity.LaunchAndDropPlayers", this.config_launchAndDropPlayers);
        outConfig.set("PopulationDensity.LaunchAndDropNewPlayers", this.config_launchAndDropNewPlayers);
        outConfig.set("PopulationDensity.TeleportAnimals", this.config_teleportAnimals);
        outConfig.set("PopulationDensity.MinimumRegionPostY", this.minimumRegionPostY);
        outConfig.set("PopulationDensity.PreciseWorldSpawn", this.preciseWorldSpawn);
        outConfig.set("PopulationDensity.MinimumWoodAvailableToPlaceNewPlayers", this.woodMinimum);
        outConfig.set("PopulationDensity.MinimumResourceScoreToPlaceNewPlayers", this.resourceMinimum);
        outConfig.set("PopulationDensity.PostProtectionDistance", this.postProtectionRadius);
        outConfig.set("PopulationDensity.Maximum Region Name Length", this.maxRegionNameLength);
        outConfig.set("PopulationDensity.PostDesign.TopBlock", top);
        outConfig.set("PopulationDensity.PostDesign.MidTopBlock", midTop);
        outConfig.set("PopulationDensity.PostDesign.MidBottomBlock", midBottom);
        outConfig.set("PopulationDensity.PostDesign.BottomBlock", bottom);
        outConfig.set("PopulationDensity.PostDesign.PlatformOuterRing", outerPlat);
        outConfig.set("PopulationDensity.PostDesign.PlatformInnerRing", innerPlat);
        outConfig.set("PopulationDensity.Region Name List", regionNames);

        //this is a combination load/preprocess/save for custom signs on the region posts
        this.topSignContent = this.initializeSignContentConfig(config, outConfig, "PopulationDensity.Signs.Top",
                new String[] { "", "%regionName%", "Region", "" });
        this.sideSignContent = this.initializeSignContentConfig(config, outConfig, "PopulationDensity.Signs.Side",
                new String[] { "<--", "%regionName%", "Region", "<--" });
        this.instructionsSignContent = this.initializeSignContentConfig(config, outConfig,
                "PopulationDensity.Signs.Instructions",
                new String[] { "Teleport", "From Here!", "Punch For", "Instructions" });

        this.mainCustomSignContent = this.initializeSignContentConfig(config, outConfig,
                "PopulationDensity.CustomSigns.Main", new String[] { "", "Population", "Density", "" });
        this.northCustomSignContent = this.initializeSignContentConfig(config, outConfig,
                "PopulationDensity.CustomSigns.North", new String[] { "", "", "", "" });
        this.southCustomSignContent = this.initializeSignContentConfig(config, outConfig,
                "PopulationDensity.CustomSigns.South", new String[] { "", "", "", "" });
        this.eastCustomSignContent = this.initializeSignContentConfig(config, outConfig,
                "PopulationDensity.CustomSigns.East", new String[] { "", "", "", "" });
        this.westCustomSignContent = this.initializeSignContentConfig(config, outConfig,
                "PopulationDensity.CustomSigns.West", new String[] { "", "", "", "" });

        try {
            outConfig.save(DataStore.configFilePath);
        } catch (IOException exception) {
            AddLogEntry("Unable to write to the configuration file at \"" + DataStore.configFilePath + "\"");
        }

        //get a reference to the managed world
        if (this.managedWorldName == null || this.managedWorldName.isEmpty()) {
            PopulationDensity.AddLogEntry("Please specify a world to manage in config.yml.");
            return;
        }
        ManagedWorld = this.getServer().getWorld(this.managedWorldName);
        if (ManagedWorld == null) {
            PopulationDensity.AddLogEntry("Could not find a world named \"" + this.managedWorldName
                    + "\".  Please update your config.yml.");
            return;
        }

        //when datastore initializes, it loads player and region data, and posts some stats to the log
        this.dataStore = new DataStore(this.config_regionNames);

        //register for events
        PluginManager pluginManager = this.getServer().getPluginManager();

        //set up region name tab completers
        PluginCommand visitCommand = this.getCommand("visit");
        visitCommand.setTabCompleter(this.dataStore);

        //player events, to control spawn, respawn, disconnect, and region-based notifications as players walk around
        PlayerEventHandler playerEventHandler = new PlayerEventHandler(this.dataStore, this);
        pluginManager.registerEvents(playerEventHandler, this);

        //block events, to limit building around region posts and in some other cases (config dependent)
        BlockEventHandler blockEventHandler = new BlockEventHandler();
        pluginManager.registerEvents(blockEventHandler, this);

        //entity events, to protect region posts from explosions
        EntityEventHandler entityEventHandler = new EntityEventHandler(this);
        pluginManager.registerEvents(entityEventHandler, this);

        //world events, to generate region posts when chunks load
        WorldEventHandler worldEventHandler = new WorldEventHandler();
        pluginManager.registerEvents(worldEventHandler, this);

        //Only load listeners if LaunchAndDrop is enabled in config
        if (config_launchAndDropPlayers || config_launchAndDropNewPlayers) {
            this.dropShipTeleporterInstance = new DropShipTeleporter(this);
            pluginManager.registerEvents(dropShipTeleporterInstance, this);
        }

        //make a note of the spawn world.  may be NULL if the configured city world name doesn't match an existing world
        CityWorld = this.getServer().getWorld(this.cityWorldName);
        if (!this.cityWorldName.isEmpty() && CityWorld == null) {
            PopulationDensity.AddLogEntry(
                    "Could not find a world named \"" + this.cityWorldName + "\".  Please update your config.yml.");
        }

        //scan the open region for resources and open a new one as necessary
        //may open and close several regions before finally leaving an "acceptable" region open
        //this will repeat every six hours
        this.getServer().getScheduler().scheduleSyncRepeatingTask(this, new ScanOpenRegionTask(), 5L,
                this.hoursBetweenScans * 60 * 60 * 20L);

        //start monitoring performance
        this.getServer().getScheduler().scheduleSyncRepeatingTask(this, new MonitorPerformanceTask(), 1200L, 1200L);

        //animals which appear abandoned on chunk load get the grandfather clause treatment
        for (World world : this.getServer().getWorlds()) {
            for (Chunk chunk : world.getLoadedChunks()) {
                Entity[] entities = chunk.getEntities();
                for (Entity entity : entities) {
                    if (WorldEventHandler.isAbandoned(entity)) {
                        entity.setTicksLived(1);
                    }
                }
            }
        }
        try {
            Metrics metrics = new Metrics(this);
            metrics.addCustomChart(new Metrics.SimplePie("bukkit_impl", new Callable<String>() {
                @Override
                public String call() throws Exception {
                    return getServer().getVersion().split("-")[1];
                }
            }));
        } catch (Throwable ignored) {
        }
    }

    String getRegionNameError(String name, boolean console) {
        if (name.length() > this.maxRegionNameLength) {
            if (console)
                return "Name too long.";
            else
                return this.dataStore.getMessage(Messages.RegionNameLength, String.valueOf(maxRegionNameLength));
        }

        for (int i = 0; i < name.length(); i++) {
            char c = name.charAt(i);
            if (!Character.isLetter(c) && !Character.isDigit(c) && c != ' ') {
                if (console)
                    return "Name includes symbols or puncutation.";
                else
                    return this.dataStore.getMessage(Messages.RegionNamesOnlyLettersAndNumbers);
            }
        }

        return null;
    }

    public String[] initializeSignContentConfig(FileConfiguration config, FileConfiguration outConfig,
            String configurationNode, String[] defaultLines) {
        //read what's in the file
        List<String> linesFromConfig = config.getStringList(configurationNode);

        //if nothing, replace with default
        int i = 0;
        if (linesFromConfig == null || linesFromConfig.size() == 0) {
            for (; i < defaultLines.length && i < 4; i++) {
                linesFromConfig.add(defaultLines[i]);
            }
        }

        //fill any blanks
        for (i = linesFromConfig.size(); i < 4; i++) {
            linesFromConfig.add("");
        }

        //write it back to the config file
        outConfig.set(configurationNode, linesFromConfig);

        //would the sign be empty?
        boolean emptySign = true;
        for (i = 0; i < 4; i++) {
            if (linesFromConfig.get(i).length() > 0) {
                emptySign = false;
                break;
            }
        }

        //return end result
        if (emptySign) {
            return null;
        } else {
            String[] returnArray = new String[4];
            for (i = 0; i < 4 && i < linesFromConfig.size(); i++) {
                returnArray[i] = linesFromConfig.get(i);
            }

            return returnArray;
        }
    }

    public boolean onCommand(CommandSender sender, Command cmd, String commandLabel, String[] args) {
        Player player = null;
        PlayerData playerData = null;
        if (sender instanceof Player) {
            player = (Player) sender;

            if (ManagedWorld == null) {
                PopulationDensity.sendMessage(player, TextMode.Err, Messages.NoManagedWorld);
                return true;
            }

            playerData = this.dataStore.getPlayerData(player);
        }

        if (cmd.getName().equalsIgnoreCase("visit") && player != null) {
            if (args.length < 1)
                return false;

            CanTeleportResult result = this.playerCanTeleport(player, false);
            if (!result.canTeleport)
                return true;

            @SuppressWarnings("deprecation")
            Player targetPlayer = this.getServer().getPlayerExact(args[0]);
            if (targetPlayer != null && player.canSee(targetPlayer)) {
                PlayerData targetPlayerData = this.dataStore.getPlayerData(targetPlayer);
                if (playerData.inviter != null && playerData.inviter.getName().equals(targetPlayer.getName())) {
                    if (result.nearPost && this.launchPlayer(player)) {
                        this.TeleportPlayer(player, targetPlayerData.homeRegion, 1);
                    } else {
                        this.TeleportPlayer(player, targetPlayerData.homeRegion, 0);
                    }

                } else if (this.dataStore.getRegionName(targetPlayerData.homeRegion) == null) {
                    PopulationDensity.sendMessage(player, TextMode.Err, Messages.InvitationNeeded,
                            targetPlayer.getName());
                    return true;
                } else {
                    if (this.launchPlayer(player)) {
                        this.TeleportPlayer(player, targetPlayerData.homeRegion, 1);
                    } else {
                        this.TeleportPlayer(player, targetPlayerData.homeRegion, 0);
                    }
                }

                PopulationDensity.sendMessage(player, TextMode.Success, Messages.VisitConfirmation,
                        targetPlayer.getName());
            } else {
                //find the specified region, and send an error message if it's not found
                String name = PopulationDensity.join(args);
                RegionCoordinates region = this.dataStore.getRegionCoordinates(name);
                if (region == null) {
                    PopulationDensity.sendMessage(player, TextMode.Err, Messages.DestinationNotFound, name);
                    return true;
                }

                //otherwise, teleport the user to the specified region
                if (this.launchPlayer(player)) {
                    this.TeleportPlayer(player, region, 1);
                } else {
                    this.TeleportPlayer(player, region, 0);
                }
            }

            return true;
        } else if (cmd.getName().equalsIgnoreCase("newestregion") && player != null) {
            //check permission, if necessary
            if (this.newestRegionRequiresPermission && !player.hasPermission("populationdensity.newestregion")) {
                PopulationDensity.sendMessage(player, TextMode.Err, Messages.NeedNewestRegionPermission);
                return true;
            }

            CanTeleportResult result = this.playerCanTeleport(player, false);
            if (!result.canTeleport)
                return true;

            //teleport the user to the open region
            RegionCoordinates openRegion = this.dataStore.getOpenRegion();
            if (result.nearPost && this.launchPlayer(player)) {
                this.TeleportPlayer(player, openRegion, 1);
            } else {
                this.TeleportPlayer(player, openRegion, 0);
            }

            PopulationDensity.sendMessage(player, TextMode.Success, Messages.NewestRegionConfirmation);

            return true;
        } else if (cmd.getName().equalsIgnoreCase("whichregion") && player != null) {
            RegionCoordinates currentRegion = RegionCoordinates.fromLocation(player.getLocation());
            if (currentRegion == null) {
                PopulationDensity.sendMessage(player, TextMode.Warn, Messages.NotInRegion);
                return true;
            }

            String regionName = this.dataStore.getRegionName(currentRegion);
            if (regionName == null) {
                PopulationDensity.sendMessage(player, TextMode.Info, Messages.UnnamedRegion);
            } else {
                PopulationDensity.sendMessage(player, TextMode.Info, Messages.WhichRegion, capitalize(regionName));
            }

            return true;
        } else if (cmd.getName().equalsIgnoreCase("listregions")) {
            PopulationDensity.sendMessage(player, TextMode.Info, this.dataStore.getRegionNames());

            return true;
        } else if (cmd.getName().equalsIgnoreCase("nameregion") && player != null) {
            return this.nameRegion(player, args, false);
        } else if (cmd.getName().equalsIgnoreCase("renameregion") && player != null) {
            return this.nameRegion(player, args, true);
        } else if (cmd.getName().equalsIgnoreCase("addregionpost") && player != null) {
            RegionCoordinates currentRegion = RegionCoordinates.fromLocation(player.getLocation());
            if (currentRegion == null) {
                PopulationDensity.sendMessage(player, TextMode.Err, Messages.NotInRegion);
                return true;
            }

            try {
                this.dataStore.AddRegionPost(currentRegion);
            } catch (ChunkLoadException e) {
            } //ignore.  post will be auto-built when the chunk is loaded later

            return true;
        } else if (cmd.getName().equalsIgnoreCase("homeregion") && player != null) {
            return this.handleHomeCommand(player, playerData);
        } else if (cmd.getName().equalsIgnoreCase("cityregion") && player != null) {
            //if city world isn't defined, send the player home
            if (CityWorld == null) {
                return this.handleHomeCommand(player, playerData);
            }

            //otherwise teleportation is enabled, so consider config, player location, player permissions
            CanTeleportResult result = this.playerCanTeleport(player, true);
            if (result.canTeleport) {
                Location spawn = CityWorld.getSpawnLocation();

                Block block = spawn.getBlock();
                while (block.getType().isSolid()) {
                    block = block.getRelative(BlockFace.UP);
                }

                if (result.nearPost && this.launchPlayer(player))
                    new TeleportPlayerTask(player, block.getLocation(), false, instance).runTaskLater(this, 20L);
                else
                    new TeleportPlayerTask(player, block.getLocation(), false, instance).runTaskLater(this, 0L);
            }

            return true;
        } else if (cmd.getName().equalsIgnoreCase("randomregion") && player != null) {
            CanTeleportResult result = this.playerCanTeleport(player, false);
            if (!result.canTeleport)
                return true;

            RegionCoordinates randomRegion = this.dataStore
                    .getRandomRegion(RegionCoordinates.fromLocation(player.getLocation()));

            if (randomRegion == null) {
                PopulationDensity.sendMessage(player, TextMode.Err, Messages.NoMoreRegions);
            } else {
                if (result.nearPost && this.launchPlayer(player)) {
                    this.TeleportPlayer(player, randomRegion, 1);
                } else {
                    this.TeleportPlayer(player, randomRegion, 0);
                }

            }

            return true;
        } else if (cmd.getName().equalsIgnoreCase("invite") && player != null) {
            if (args.length < 1)
                return false;

            //send a notification to the invitee, if he's available
            @SuppressWarnings("deprecation")
            Player invitee = this.getServer().getPlayer(args[0]);
            if (invitee != null && player.canSee(invitee)) {
                playerData = this.dataStore.getPlayerData(invitee);
                if (playerData.inviter == player) {
                    PopulationDensity.sendMessage(player, TextMode.Success, Messages.InviteAlreadySent,
                            invitee.getName(), player.getName());
                    return true;
                }
                playerData.inviter = player;
                PopulationDensity.sendMessage(player, TextMode.Success, Messages.InviteConfirmation,
                        invitee.getName(), player.getName());
                PopulationDensity.sendMessage(invitee, TextMode.Success, Messages.InviteNotification,
                        player.getName());
                PopulationDensity.sendMessage(invitee, TextMode.Instr, Messages.InviteInstruction,
                        player.getName());
            } else {
                PopulationDensity.sendMessage(player, TextMode.Err, Messages.PlayerNotFound, args[0]);
            }

            return true;
        } else if (cmd.getName().equalsIgnoreCase("sendregion")) {
            if (args.length < 1)
                return false;

            @SuppressWarnings("deprecation")
            Player targetPlayer = this.getServer().getPlayerExact(args[0]);
            if (targetPlayer != null) {
                playerData = this.dataStore.getPlayerData(targetPlayer);
                RegionCoordinates destination = playerData.homeRegion;
                if (args.length > 1) {
                    String name = PopulationDensity.join(args, 1);
                    destination = this.dataStore.getRegionCoordinates(name);
                    if (destination == null) {
                        PopulationDensity.sendMessage(player, TextMode.Err, Messages.DestinationNotFound, name);
                        return true;
                    }
                }

                //otherwise, teleport the target player to the destination region                  
                this.TeleportPlayer(targetPlayer, destination, 0);
                PopulationDensity.sendMessage(player, TextMode.Success, Messages.PlayerMoved);
            } else {
                PopulationDensity.sendMessage(player, TextMode.Err, Messages.PlayerNotFound, args[0]);
            }

            return true;
        } else if (cmd.getName().equalsIgnoreCase("sethomeregion") && player != null) {
            //if not in the managed world, /movein doesn't make sense
            if (!player.getWorld().equals(ManagedWorld)) {
                PopulationDensity.sendMessage(player, TextMode.Err, Messages.NotInRegion);
                return true;
            }

            playerData.homeRegion = RegionCoordinates.fromLocation(player.getLocation());
            this.dataStore.savePlayerData(player, playerData);
            PopulationDensity.sendMessage(player, TextMode.Success, Messages.SetHomeConfirmation);
            PopulationDensity.sendMessage(player, TextMode.Instr, Messages.SetHomeInstruction1);
            PopulationDensity.sendMessage(player, TextMode.Instr, Messages.SetHomeInstruction2);

            return true;
        } else if (cmd.getName().equalsIgnoreCase("addregion") && player != null) {
            PopulationDensity.sendMessage(player, TextMode.Success, Messages.AddRegionConfirmation);

            RegionCoordinates newRegion = this.dataStore.addRegion();

            this.scanRegion(newRegion, true);

            return true;
        } else if (cmd.getName().equalsIgnoreCase("scanregion") && player != null) {
            PopulationDensity.sendMessage(player, TextMode.Success, Messages.ScanStartConfirmation);
            this.scanRegion(RegionCoordinates.fromLocation(player.getLocation()), false);

            return true;
        } else if (cmd.getName().equalsIgnoreCase("loginpriority")) {
            //requires exactly two parameters, the other player's name and the priority
            if (args.length != 2 && args.length != 1)
                return false;

            PlayerData targetPlayerData = null;
            OfflinePlayer targetPlayer = null;
            if (args.length > 0) {
                //find the specified player
                targetPlayer = this.resolvePlayer(args[0]);
                if (targetPlayer == null) {
                    PopulationDensity.sendMessage(player, TextMode.Err, Messages.PlayerNotFound, args[0]);
                    return true;
                }

                targetPlayerData = this.dataStore.getPlayerData(targetPlayer);

                PopulationDensity.sendMessage(player, TextMode.Info, Messages.LoginPriorityCheck,
                        targetPlayer.getName(), String.valueOf(targetPlayerData.loginPriority));

                if (args.length < 2)
                    return false; //usage displayed

                //parse the adjustment amount
                int priority;
                try {
                    priority = Integer.parseInt(args[1]);
                } catch (NumberFormatException numberFormatException) {
                    return false; //causes usage to be displayed
                }

                //set priority
                if (priority > 100)
                    priority = 100;
                else if (priority < 0)
                    priority = 0;

                targetPlayerData.loginPriority = priority;
                this.dataStore.savePlayerData(targetPlayer, targetPlayerData);

                //confirmation message
                PopulationDensity.sendMessage(player, TextMode.Success, Messages.LoginPriorityUpdate,
                        targetPlayer.getName(), String.valueOf(priority));

                return true;
            }
        } else if (cmd.getName().equalsIgnoreCase("thinentities")) {
            if (player != null) {
                PopulationDensity.sendMessage(player, TextMode.Success, Messages.ThinningConfirmation);
            }

            MonitorPerformanceTask.thinEntities();

            return true;
        } else if (cmd.getName().equalsIgnoreCase("simlag") && player == null) {
            float tps;
            try {
                tps = Float.parseFloat(args[0]);
            } catch (NumberFormatException e) {
                return false;
            }

            MonitorPerformanceTask.treatLag(tps);
            return true;
        } else if (cmd.getName().equalsIgnoreCase("lag")) {
            this.reportTPS(player);

            return true;
        }

        return false;
    }

    private boolean nameRegion(Player player, String[] args, boolean allowRename) {
        RegionCoordinates currentRegion = RegionCoordinates.fromLocation(player.getLocation());
        if (currentRegion == null) {
            PopulationDensity.sendMessage(player, TextMode.Warn, Messages.NotInRegion);
            return true;
        }

        if (!allowRename) {
            String name = this.dataStore.getRegionName(currentRegion);
            if (name != null) {
                PopulationDensity.sendMessage(player, TextMode.Err, Messages.RegionAlreadyNamed);
                return true;
            }
        }

        //validate argument
        if (args.length < 1)
            return false;
        String name = PopulationDensity.join(args);

        if (this.dataStore.getRegionCoordinates(name) != null) {
            PopulationDensity.sendMessage(player, TextMode.Err, Messages.RegionNameConflict);
            return true;
        }

        //name region
        try {
            this.dataStore.nameRegion(currentRegion, name);
        } catch (RegionNameException e) {
            PopulationDensity.sendMessage(player, TextMode.Err, e.getMessage());
            return true;
        }

        //update post
        try {
            this.dataStore.AddRegionPost(currentRegion);
        } catch (ChunkLoadException e) {
        } //ignore.  post will be auto-rebuilt when the chunk is loaded later

        return true;
    }

    void reportTPS(Player player) {
        String message = PopulationDensity.instance.dataStore.getMessage(Messages.PerformanceScore,
                String.valueOf(Math.round((serverTicksPerSecond / 20) * 100)));
        if (serverTicksPerSecond > 19) {
            message = PopulationDensity.instance.dataStore.getMessage(Messages.PerformanceScore_NoLag) + message;
        } else {
            message += PopulationDensity.instance.dataStore.getMessage(Messages.PerformanceScore_Lag);
        }

        if (player != null) {
            player.sendMessage(ChatColor.GOLD + message);
        } else {
            AddLogEntry(message);
        }
    }

    private static String join(String[] args, int offset) {
        StringBuilder builder = new StringBuilder();
        for (int i = offset; i < args.length; i++) {
            builder.append(args[i]).append(" ");
        }

        return builder.toString().trim();
    }

    private static String join(String[] args) {
        return join(args, 0);
    }

    private boolean handleHomeCommand(Player player, PlayerData playerData) {
        //consider config, player location, player permissions
        CanTeleportResult result = this.playerCanTeleport(player, true);
        if (result.canTeleport) {
            RegionCoordinates homeRegion = playerData.homeRegion;
            if (result.nearPost && this.launchPlayer(player)) {
                this.TeleportPlayer(player, homeRegion, 1);
            } else {
                this.TeleportPlayer(player, homeRegion, 0);
            }
            return true;
        }

        return true;
    }

    public void onDisable() {
        AddLogEntry("PopulationDensity disabled.");
    }

    //examines configuration, player permissions, and player location to determine whether or not to allow a teleport
    @SuppressWarnings("deprecation")
    private CanTeleportResult playerCanTeleport(Player player, boolean isHomeOrCityTeleport) {
        //if the player has the permission for teleportation, always allow it
        if (player.hasPermission("populationdensity.teleportanywhere"))
            return new CanTeleportResult(true);

        //disallow spamming commands to hover in the air
        if (!player.isOnGround()) {
            if (player.getVehicle() == null) {
                return new CanTeleportResult(false);
            }
            // Dismount them if they're riding
            player.leaveVehicle();
        }

        //if teleportation from anywhere is enabled, always allow it
        if (this.teleportFromAnywhere)
            return new CanTeleportResult(true);

        //avoid teleporting from other worlds
        if (!player.getWorld().equals(ManagedWorld)
                && (CityWorld == null || !player.getWorld().equals(CityWorld))) {
            PopulationDensity.sendMessage(player, TextMode.Err, Messages.NoTeleportThisWorld);
            return new CanTeleportResult(false);
        }

        //when teleportation isn't allowed, the only exceptions are city to home, and home to city
        if (!this.allowTeleportation) {
            if (!isHomeOrCityTeleport) {
                PopulationDensity.sendMessage(player, TextMode.Err, Messages.OnlyHomeCityHere);
                return new CanTeleportResult(false);
            }

            //if city is defined and close to city post, go for it
            if (nearCityPost(player)) {
                CanTeleportResult result = new CanTeleportResult(true);
                result.nearCityPost = true;
                return result;
            }

            //if close to home post, go for it
            PlayerData playerData = this.dataStore.getPlayerData(player);
            Location homeCenter = getRegionCenter(playerData.homeRegion, false);
            Location location = player.getLocation();
            if (location.getBlockY() >= PopulationDensity.instance.minimumRegionPostY
                    && Math.abs(location.getBlockX() - homeCenter.getBlockX()) < 2
                    && Math.abs(location.getBlockZ() - homeCenter.getBlockZ()) < 2
                    && location.getBlock().getLightFromSky() > 0) {
                CanTeleportResult result = new CanTeleportResult(true);
                result.nearPost = true;
                return result;
            }

            PopulationDensity.sendMessage(player, TextMode.Err, Messages.NoTeleportHere);
            return new CanTeleportResult(false);
        }

        //otherwise, any post is acceptable to teleport from or to
        else {
            if (nearCityPost(player)) {
                CanTeleportResult result = new CanTeleportResult(true);
                result.nearCityPost = true;
                return result;
            }

            RegionCoordinates currentRegion = RegionCoordinates.fromLocation(player.getLocation());
            Location currentCenter = getRegionCenter(currentRegion, false);
            Location location = player.getLocation();
            if (location.getBlockY() >= PopulationDensity.instance.minimumRegionPostY
                    && Math.abs(location.getBlockX() - currentCenter.getBlockX()) < 3
                    && Math.abs(location.getBlockZ() - currentCenter.getBlockZ()) < 3
                    && location.getBlock().getLightFromSky() > 0) {
                CanTeleportResult result = new CanTeleportResult(true);
                result.nearPost = true;
                return result;
            }

            PopulationDensity.sendMessage(player, TextMode.Err, Messages.NotCloseToPost);
            PopulationDensity.sendMessage(player, TextMode.Instr, Messages.HelpMessage1,
                    ChatColor.UNDERLINE + "" + ChatColor.AQUA + "http://bit.ly/mcregions");
            return new CanTeleportResult(false);
        }
    }

    private boolean nearCityPost(Player player) {
        if (CityWorld == null || !player.getWorld().equals(CityWorld))
            return false;

        //max distance == 0 indicates no distance maximum
        if (this.maxDistanceFromSpawnToUseHomeRegion < 1)
            return true;

        return player.getLocation()
                .distance(CityWorld.getSpawnLocation()) < this.maxDistanceFromSpawnToUseHomeRegion;
    }

    //teleports a player to a specific region of the managed world, notifying players of arrival/departure as necessary
    //players always land at the region's region post, which is placed on the surface at the center of the region
    public void TeleportPlayer(Player player, RegionCoordinates region, int delaySeconds) {
        TeleportPlayerToRegion(player, region, delaySeconds, config_launchAndDropPlayers);
    }

    public void TeleportPlayerToRegion(Player player, RegionCoordinates region, int delaySeconds, Boolean doDrop) {
        //where specifically to send the player?
        Location teleportDestination = getRegionCenter(region, true);
        teleportDestination.setX(teleportDestination.getBlockX() + 0.5D);
        teleportDestination.setY(teleportDestination.getBlockY() + 1D);
        teleportDestination.setZ(teleportDestination.getBlockZ() + 0.5D);

        // Check the world border
        WorldBorder border = ManagedWorld.getWorldBorder();
        double size = border.getSize() / 2;
        Location center = border.getCenter();
        double x = teleportDestination.getBlockX() - center.getX(),
                z = teleportDestination.getBlockZ() - center.getZ();
        if ((x > size || (-x) > size) || (z > size || (-z) > size)) {
            PopulationDensity.sendMessage(player, TextMode.Err, Messages.OutsideWorldBorder);
            return;
        }

        //drop the player from the sky //RoboMWM - only if LaunchAndDropPlayers is enabled
        if (doDrop && !player.getGameMode().equals(GameMode.SPECTATOR))
            teleportDestination = new Location(ManagedWorld, teleportDestination.getX(),
                    ManagedWorld.getMaxHeight() + 10, teleportDestination.getZ(), player.getLocation().getYaw(),
                    90);
        new TeleportPlayerTask(player, teleportDestination, doDrop, instance, dropShipTeleporterInstance)
                .runTaskLater(this, delaySeconds * 20L);

        //kill bad guys in the area
        removeMonstersAround(teleportDestination);
    }

    //scans the open region for resources and may close the region (and open a new one) if accessible resources are low
    //may repeat itself if the regions it opens are also not acceptably rich in resources
    //TODO: use Paper API to get chunks async (with fallback to spigot/CB)
    public void scanRegion(RegionCoordinates region, boolean openNewRegions) {
        AddLogEntry("Examining available resources in region \"" + region.toString() + "\"...");

        Location regionCenter = getRegionCenter(region, false);
        int min_x = regionCenter.getBlockX() - REGION_SIZE / 2;
        int max_x = regionCenter.getBlockX() + REGION_SIZE / 2;
        int min_z = regionCenter.getBlockZ() - REGION_SIZE / 2;
        int max_z = regionCenter.getBlockZ() + REGION_SIZE / 2;

        Chunk lesserBoundaryChunk = ManagedWorld.getChunkAt(new Location(ManagedWorld, min_x, 1, min_z));
        Chunk greaterBoundaryChunk = ManagedWorld.getChunkAt(new Location(ManagedWorld, max_x, 1, max_z));

        ChunkSnapshot[][] snapshots = new ChunkSnapshot[greaterBoundaryChunk.getX() - lesserBoundaryChunk.getX()
                + 1][greaterBoundaryChunk.getZ() - lesserBoundaryChunk.getZ() + 1];
        for (int x = 0; x < snapshots.length; x++) {
            for (int z = 0; z < snapshots[0].length; z++) {
                snapshots[x][z] = null;
            }
        }

        for (int x = 0; x < snapshots.length; x++) {
            for (int z = 0; z < snapshots[0].length; z++) {
                //skip chunks that we already have snapshots for
                if (snapshots[x][z] != null)
                    continue;

                //get the chunk, load it, generate it if necessary
                Chunk chunk = ManagedWorld.getChunkAt(x + lesserBoundaryChunk.getX(),
                        z + lesserBoundaryChunk.getZ());
                if (chunk.isLoaded() || chunk.load(true)) {
                    //take a snapshot
                    ChunkSnapshot snapshot = chunk.getChunkSnapshot();
                    snapshots[x][z] = snapshot;
                }
            }
        }

        //create a new task with this information, which will more completely scan the content of all the snapshots
        ScanRegionTask task = new ScanRegionTask(snapshots, openNewRegions);
        task.setPriority(Thread.MIN_PRIORITY);

        //run it in a separate thread
        task.start();
    }

    //ensures a piece of the managed world is loaded into server memory
    //(generates the chunk if necessary)
    //these coordinate params are BLOCK coordinates, not CHUNK coordinates
    public static void GuaranteeChunkLoaded(int x, int z) throws ChunkLoadException {
        Location location = new Location(ManagedWorld, x, 5, z);
        Chunk chunk = ManagedWorld.getChunkAt(location);
        if (!chunk.isLoaded()) {
            if (!chunk.load(true)) {
                throw new ChunkLoadException();
            }
        }
    }

    //determines the center of a region (as a Location) given its region coordinates
    //keeping all regions the same size and aligning them in a grid keeps this calculation simple and fast
    public static Location getRegionCenter(RegionCoordinates region, boolean computeY) {
        int x, z;
        x = region.x * REGION_SIZE + REGION_SIZE / 2;
        z = region.z * REGION_SIZE + REGION_SIZE / 2;

        Location center = new Location(ManagedWorld, x, PopulationDensity.instance.minimumRegionPostY, z);

        if (computeY)
            center = ManagedWorld.getHighestBlockAt(center).getLocation();

        return center;
    }

    //capitalizes a string, used to make region names pretty
    public static String capitalize(String string) {
        if (string == null || string.length() == 0)

            return string;

        if (string.length() == 1)

            return string.toUpperCase();

        //return string.substring(0, 1).toUpperCase() + string.substring(1);
        return WordUtils.capitalize(string);
    }

    public void resetIdleTimer(Player player) {
        //if idle kick is disabled, don't do anything here
        if (PopulationDensity.instance.maxIdleMinutes < 1)
            return;

        PlayerData playerData = this.dataStore.getPlayerData(player);

        //if there's a task already in the queue for this player, cancel it
        if (playerData.afkCheckTaskID >= 0) {
            PopulationDensity.instance.getServer().getScheduler().cancelTask(playerData.afkCheckTaskID);
        }

        //queue a new task for later
        //note: 20L ~ 1 second
        playerData.afkCheckTaskID = PopulationDensity.instance.getServer().getScheduler().scheduleSyncDelayedTask(
                PopulationDensity.instance, new AfkCheckTask(player, playerData),
                20L * 60 * PopulationDensity.instance.maxIdleMinutes);
    }

    private OfflinePlayer resolvePlayer(String name) {
        @SuppressWarnings("deprecation")
        Player player = this.getServer().getPlayer(name);
        if (player != null)
            return player;

        OfflinePlayer[] offlinePlayers = this.getServer().getOfflinePlayers();
        for (int i = 0; i < offlinePlayers.length; i++) {
            if (offlinePlayers[i].getName().equalsIgnoreCase(name)) {
                return offlinePlayers[i];
            }
        }

        return null;
    }

    static void removeMonstersAround(Location location) {
        Chunk centerChunk = location.getChunk();
        World world = location.getWorld();

        for (int x = centerChunk.getX() - 2; x <= centerChunk.getX() + 2; x++) {
            for (int z = centerChunk.getZ() - 2; z <= centerChunk.getZ() + 2; z++) {
                Chunk chunk = world.getChunkAt(x, z);
                for (Entity entity : chunk.getEntities()) {
                    if (entity instanceof Monster && entity.getCustomName() == null
                            && ((Monster) entity).getRemoveWhenFarAway() && !((Monster) entity).isLeashed()) {
                        entity.remove();
                    }
                }
            }
        }
    }

    private Material processMaterials(String string) {
        Material material = Material.getMaterial(string);
        if (material == null) {
            AddLogEntry("Error: Couldn't resolve material \"" + string + "\". Please update your config.yml.");
        }
        return material;
    }

    //sends a color-coded message to a player
    static void sendMessage(Player player, ChatColor color, Messages messageID, String... args) {
        sendMessage(player, color, messageID, 0, args);
    }

    //sends a color-coded message to a player
    static void sendMessage(Player player, ChatColor color, Messages messageID, long delayInTicks, String... args) {
        String message = PopulationDensity.instance.dataStore.getMessage(messageID, args);
        sendMessage(player, color, message, delayInTicks);
    }

    //sends a color-coded message to a player
    static void sendMessage(Player player, ChatColor color, String message) {
        if (message == null || message.length() == 0)
            return;

        if (player == null) {
            PopulationDensity.AddLogEntry(color + message);
        } else {
            player.sendMessage(color + message);
        }
    }

    static void sendMessage(Player player, ChatColor color, String message, long delayInTicks) {
        SendPlayerMessageTask task = new SendPlayerMessageTask(player, color, message);
        if (delayInTicks > 0)
            PopulationDensity.instance.getServer().getScheduler().runTaskLater(PopulationDensity.instance, task,
                    delayInTicks);
        else
            task.run();
    }

    boolean launchPlayer(Player player) {
        if (!config_launchAndDropPlayers)
            return false;
        if (player.isFlying())
            return false;
        if (!((Entity) player).isOnGround())
            return false;
        dropShipTeleporterInstance.makeEntityFallDamageImmune(player);
        Location newViewAngle = player.getLocation();
        newViewAngle.setPitch(90);
        player.teleport(newViewAngle);
        player.setVelocity(new Vector(0, 4, 0));
        player.playSound(player.getEyeLocation(), Sound.ENTITY_GHAST_SHOOT, .75f, 1f);
        player.setGliding(false);
        return true;
    }
}