/* 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 java.io.File; import java.io.IOException; import java.util.AbstractMap.SimpleEntry; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.UUID; import java.util.logging.Logger; import org.bukkit.Bukkit; 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.block.Block; import org.bukkit.block.BlockFace; import org.bukkit.command.*; import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.entity.Entity; import org.bukkit.entity.EntityType; import org.bukkit.entity.LivingEntity; import org.bukkit.entity.Monster; import org.bukkit.entity.Player; import org.bukkit.metadata.FixedMetadataValue; import org.bukkit.plugin.PluginManager; import org.bukkit.plugin.java.JavaPlugin; import org.bukkit.util.Vector; public class PopulationDensity extends JavaPlugin { //for convenience, a reference to the instance of this plugin public static PopulationDensity instance; //for logging to the console and log file private static Logger log = Logger.getLogger("Minecraft"); //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 preciseWorldSpawn; public int woodMinimum; public int resourceMinimum; public Integer postTopperId = 89; public Integer postTopperData = 0; public Integer postId = 89; public Integer postData = 0; public Integer outerPlatformId = 98; public Integer outerPlatformData = 0; public Integer innerPlatformId = 98; public Integer innerPlatformData = 0; public int nearbyMonsterSpawnLimit; public int maxRegionNameLength = 10; public boolean abandonedFarmAnimalsDie; public boolean unusedMinecartsVanish; public boolean markRemovedEntityLocations; public boolean removeWildSkeletalHorses; public boolean config_captureSpigotTimingsWhenLagging = true; public boolean config_bootIdlePlayersWhenLagging; public boolean config_disableGrindersWhenLagging; public int config_maximumHoppersPerChunk; public int minimumRegionPostY; 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("PopDensity: " + entry); } //initializes well... everything public void onEnable() { AddLogEntry("PopulationDensity enabled."); instance = this; 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.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", true); String topper = config.getString("PopulationDensity.PostDesign.TopBlock", "89:0"); //default glowstone String post = config.getString("PopulationDensity.PostDesign.PostBlocks", "89:0"); String outerPlat = config.getString("PopulationDensity.PostDesign.PlatformOuterRing", "98:0"); //default stone brick String innerPlat = config.getString("PopulationDensity.PostDesign.PlatformInnerRing", "98:0"); 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.Mark Abandoned Removed Animal Locations With Shrubs", true); this.removeWildSkeletalHorses = config.getBoolean("PopulationDensity.Remove Wild Skeletal Horses", true); SimpleEntry<Integer, Integer> result; result = this.processMaterials(topper); if(result != null) { this.postTopperId = result.getKey(); this.postTopperData = result.getValue(); } result = this.processMaterials(post); if(result != null) { this.postId = result.getKey(); this.postData = result.getValue(); } result = this.processMaterials(outerPlat); if(result != null) { this.outerPlatformId = result.getKey(); this.outerPlatformData = result.getValue(); } result = this.processMaterials(innerPlat); if(result != null) { this.innerPlatformId = result.getKey(); this.innerPlatformData = result.getValue(); } 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.Abandoned Farm Animals Die", this.abandonedFarmAnimalsDie); outConfig.set("PopulationDensity.Unused Minecarts Vanish", this.unusedMinecartsVanish); outConfig.set("PopulationDensity.Mark Removed Animal Locations With Shrubs", 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.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", topper); outConfig.set("PopulationDensity.PostDesign.PostBlocks", post); 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.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(); pluginManager.registerEvents(entityEventHandler, this); //world events, to generate region posts when chunks load WorldEventHandler worldEventHandler = new WorldEventHandler(); pluginManager.registerEvents(worldEventHandler, 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); } } } } } 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) { 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, 2); } 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, 2); } 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, 2); } 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, 2); } 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)) { Bukkit.getScheduler().scheduleSyncDelayedTask(this, new TeleportPlayerTask(player, block.getLocation(), false), 60L); } else { Bukkit.getScheduler().scheduleSyncDelayedTask(this, new TeleportPlayerTask(player, block.getLocation(), false), 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, 2); } 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) { playerData = this.dataStore.getPlayerData(invitee); playerData.inviter = player; PopulationDensity.sendMessage(player, TextMode.Success, Messages.InviteConfirmation, invitee.getName(), 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("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, 2); } else { this.TeleportPlayer(player, randomRegion, 0); } } 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, 2); } 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(PopulationDensity.instance.isFallDamageImmune(player) && !player.isOnGround()) return new CanTeleportResult(false); //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) { //where specifically to send the player? Location teleportDestination = getRegionCenter(region, false); int x = teleportDestination.getBlockX(); int z = teleportDestination.getBlockZ(); //drop the player from the sky teleportDestination = new Location(ManagedWorld, x, ManagedWorld.getMaxHeight() + 10, z, player.getLocation().getYaw(), 90); TeleportPlayerTask task = new TeleportPlayerTask(player, teleportDestination, true); Bukkit.getScheduler().scheduleSyncDelayedTask(this, task, delaySeconds * 20); //kill bad guys in the area PopulationDensity.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 @SuppressWarnings("deprecation") 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; } } boolean snapshotIncomplete; do { snapshotIncomplete = false; 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(); //verify the snapshot by finding something that's not air boolean foundNonAir = false; for(int y = 0; y < ManagedWorld.getMaxHeight(); y++) { //if we find something, save the snapshot to the snapshot array if(snapshot.getBlockTypeId(0, y, 0) != Material.AIR.getId()) { foundNonAir = true; snapshots[x][z] = snapshot; break; } } //otherwise, plan to repeat this process again after sleeping a bit if(!foundNonAir) { snapshotIncomplete = true; } } else { snapshotIncomplete = true; } } } //if at least one snapshot was all air, sleep a second to let the chunk loader/generator //catch up, and then try again if(snapshotIncomplete) { try { Thread.sleep(1000); } catch (InterruptedException e) { } } }while(snapshotIncomplete); //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; if(region.x >= 0) x = region.x * REGION_SIZE + REGION_SIZE / 2; else x = region.x * REGION_SIZE + REGION_SIZE / 2; if(region.z >= 0) z = region.z * REGION_SIZE + REGION_SIZE / 2; else 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); } 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 SimpleEntry<Integer, Integer> processMaterials(String string) { String [] elements = string.split(":"); if(elements.length < 2) { PopulationDensity.AddLogEntry("Couldn't understand config entry '" + string + "'. Use format 'id:data'."); return null; } try { int id_output = Integer.parseInt(elements[0]); int data_output = Integer.parseInt(elements[1]); return new SimpleEntry<Integer, Integer>(id_output, data_output); } catch(NumberFormatException e) { PopulationDensity.AddLogEntry("Couldn't understand config entry '" + string + "'. Use format 'id:data'."); } return null; } //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(); } } HashSet<UUID> fallImmunityList = new HashSet<UUID>(); void makeEntityFallDamageImmune(LivingEntity entity) { if(entity.getType() == EntityType.PLAYER) { Player player = (Player) entity; if(player.getGameMode() == GameMode.CREATIVE || player.getGameMode() == GameMode.SPECTATOR) return; player.setGliding(false); } entity.setGliding(false); entity.setMetadata("PD_NOFALLDMG", new FixedMetadataValue(this, true)); fallImmunityList.add(entity.getUniqueId()); } boolean isFallDamageImmune(Entity entity) { return entity.hasMetadata("PD_NOFALLDMG") || fallImmunityList.contains(entity.getUniqueId()); } void removeFallDamageImmunity(Entity entity) { entity.removeMetadata("PD_NOFALLDMG", this); fallImmunityList.remove(entity.getUniqueId()); } boolean launchPlayer(Player player) { if(player.isFlying()) return false; if(!((Entity)player).isOnGround()) return false; this.makeEntityFallDamageImmune(player); Location newViewAngle = player.getLocation(); newViewAngle.setPitch(90); player.teleport(newViewAngle); player.setVelocity(new Vector(0, 50, 0)); player.playSound(player.getEyeLocation(), Sound.ENTITY_GHAST_SHOOT, .75f, 1f); player.setGliding(false); return true; } }