/*
PopulationDensity Server Plugin for Minecraft
Copyright (C) 2011 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.*;
import java.nio.charset.Charset;
import java.text.DateFormat;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import org.apache.commons.lang.Validate;
import org.bukkit.*;
import org.bukkit.block.Block;
import org.bukkit.block.BlockFace;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.entity.Player;
import org.bukkit.util.StringUtil;
import com.google.common.collect.ImmutableList;
import com.google.common.io.Files;
public class DataStore implements TabCompleter
{
//in-memory cache for player home region, because it's needed very frequently
private HashMap<String, PlayerData> playerNameToPlayerDataMap = new HashMap<String, PlayerData>();
//path information, for where stuff stored on disk is well... stored
private final static String dataLayerFolderPath = "plugins" + File.separator + "PopulationDensityData";
private final static String playerDataFolderPath = dataLayerFolderPath + File.separator + "PlayerData";
private final static String regionDataFolderPath = dataLayerFolderPath + File.separator + "RegionData";
public final static String configFilePath = dataLayerFolderPath + File.separator + "config.yml";
final static String messagesFilePath = dataLayerFolderPath + File.separator + "messages.yml";
//in-memory cache for messages
private String [] messages;
//currently open region
private RegionCoordinates openRegionCoordinates;
//coordinates of the next region which will be opened, if one needs to be opened
private RegionCoordinates nextRegionCoordinates;
//region data cache
private ConcurrentHashMap<String, RegionCoordinates> nameToCoordsMap = new ConcurrentHashMap<String, RegionCoordinates>();
private ConcurrentHashMap<RegionCoordinates, String> coordsToNameMap = new ConcurrentHashMap<RegionCoordinates, String>();
//initialization!
public DataStore(List<String> regionNames)
{
//ensure data folders exist
new File(playerDataFolderPath).mkdirs();
new File(regionDataFolderPath).mkdirs();
this.regionNamesList = regionNames.toArray(new String[]{});
this.loadMessages();
//get a list of all the files in the region data folder
//some of them are named after region names, others region coordinates
File regionDataFolder = new File(regionDataFolderPath);
File [] files = regionDataFolder.listFiles();
for(int i = 0; i < files.length; i++)
{
if(files[i].isFile()) //avoid any folders
{
try
{
//if the filename converts to region coordinates, add that region to the list of defined regions
//(this constructor throws an exception if it can't do the conversion)
RegionCoordinates regionCoordinates = new RegionCoordinates(files[i].getName());
String regionName = Files.readFirstLine(files[i], Charset.forName("UTF-8"));
this.nameToCoordsMap.put(regionName.toLowerCase(), regionCoordinates);
this.coordsToNameMap.put(regionCoordinates, regionName);
}
//catch for files named after region names
catch(Exception e){ }
}
}
//study region data and initialize both this.openRegionCoordinates and this.nextRegionCoordinates
this.findNextRegion();
//if no regions were loaded, create the first one
if(nameToCoordsMap.keySet().size() == 0)
{
PopulationDensity.AddLogEntry("Please be patient while I search for a good new player starting point!");
PopulationDensity.AddLogEntry("This initial scan could take a while, especially for worlds where players have already been building.");
this.addRegion();
}
PopulationDensity.AddLogEntry("Open region: \"" + this.getRegionName(this.getOpenRegion()) + "\" at " + this.getOpenRegion().toString() + ".");
}
//used in the spiraling code below (see findNextRegion())
private enum Direction { left, right, up, down }
//starts at region 0,0 and spirals outward until it finds a region which hasn't been initialized
//sets private variables for openRegion and nextRegion when it's done
//this may look like black magic, but seriously, it produces a tight spiral on a grid
//coding this made me reminisce about seemingly pointless computer science exercises in college
public int findNextRegion()
{
//spiral out from region coordinates 0, 0 until we find coordinates for an uninitialized region
int x = 0; int z = 0;
//keep count of the regions encountered
int regionCount = 0;
//initialization
Direction direction = Direction.down; //direction to search
int sideLength = 1; //maximum number of regions to move in this direction before changing directions
int side = 0; //increments each time we change directions. this tells us when to add length to each side
this.openRegionCoordinates = new RegionCoordinates(0, 0);
this.nextRegionCoordinates = new RegionCoordinates(0, 0);
//while the next region coordinates are taken, walk the spiral
while (this.getRegionName(this.nextRegionCoordinates) != null)
{
//loop for one side of the spiral
for (int i = 0; i < sideLength && this.getRegionName(this.nextRegionCoordinates) != null; i++)
{
regionCount++;
//converts a direction to a change in X or Z
if (direction == Direction.down) z++;
else if (direction == Direction.left) x--;
else if (direction == Direction.up) z--;
else x++;
this.openRegionCoordinates = this.nextRegionCoordinates;
this.nextRegionCoordinates = new RegionCoordinates(x, z);
}
//after finishing a side, change directions
if (direction == Direction.down) direction = Direction.left;
else if (direction == Direction.left) direction = Direction.up;
else if (direction == Direction.up) direction = Direction.right;
else direction = Direction.down;
//keep count of the completed sides
side++;
//on even-numbered sides starting with side == 2, increase the length of each side
if (side % 2 == 0) sideLength++;
}
//return total number of regions seen
return regionCount;
}
//picks a region at random (sort of)
public RegionCoordinates getRandomRegion(RegionCoordinates regionToAvoid)
{
if(this.coordsToNameMap.keySet().size() < 2) return null;
//initialize random number generator with a seed based the current time
Random randomGenerator = new Random();
ArrayList<RegionCoordinates> possibleDestinations = new ArrayList<RegionCoordinates>();
for(RegionCoordinates coords : this.coordsToNameMap.keySet())
{
if(!coords.equals(regionToAvoid))
{
possibleDestinations.add(coords);
}
}
//pick one of those regions at random
int randomRegion = randomGenerator.nextInt(possibleDestinations.size());
return possibleDestinations.get(randomRegion);
}
public void savePlayerData(OfflinePlayer player, PlayerData data)
{
//save that data in memory
this.playerNameToPlayerDataMap.put(player.getUniqueId().toString(), data);
BufferedWriter outStream = null;
try
{
//open the player's file
File playerFile = new File(playerDataFolderPath + File.separator + player.getUniqueId().toString());
playerFile.createNewFile();
outStream = new BufferedWriter(new FileWriter(playerFile));
//first line is home region coordinates
outStream.write(data.homeRegion.toString());
outStream.newLine();
//second line is last disconnection date,
//note use of the ROOT locale to avoid problems related to regional settings on the server being updated
DateFormat dateFormat = DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.FULL, Locale.ROOT);
outStream.write(dateFormat.format(data.lastDisconnect));
outStream.newLine();
//third line is login priority
outStream.write(String.valueOf(data.loginPriority));
outStream.newLine();
}
//if any problem, log it
catch(Exception e)
{
PopulationDensity.AddLogEntry("PopulationDensity: Unexpected exception saving data for player \"" + player.getName() + "\": " + e.getMessage());
}
try
{
//close the file
if(outStream != null) outStream.close();
}
catch(IOException exception){}
}
public PlayerData getPlayerData(OfflinePlayer player)
{
//first, check the in-memory cache
PlayerData data = this.playerNameToPlayerDataMap.get(player.getUniqueId().toString());
if(data != null) return data;
//if not there, try to load the player from file using UUID
loadPlayerDataFromFile(player.getUniqueId().toString(), player.getUniqueId().toString());
//check again
data = this.playerNameToPlayerDataMap.get(player.getUniqueId().toString());
if(data != null) return data;
//if still not there, try player name
loadPlayerDataFromFile(player.getName(), player.getUniqueId().toString());
//check again
data = this.playerNameToPlayerDataMap.get(player.getUniqueId().toString());
if(data != null) return data;
return new PlayerData();
}
private void loadPlayerDataFromFile(String source, String dest)
{
//load player data into memory
File playerFile = new File(playerDataFolderPath + File.separator + source);
BufferedReader inStream = null;
try
{
PlayerData playerData = new PlayerData();
inStream = new BufferedReader(new FileReader(playerFile.getAbsolutePath()));
//first line is home region coordinates
String homeRegionCoordinatesString = inStream.readLine();
//second line is date of last disconnection
String lastDisconnectedString = inStream.readLine();
//third line is login priority
String rankString = inStream.readLine();
//convert string representation of home coordinates to a proper object
RegionCoordinates homeRegionCoordinates = new RegionCoordinates(homeRegionCoordinatesString);
playerData.homeRegion = homeRegionCoordinates;
//parse the last disconnect date string
try
{
DateFormat dateFormat = DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.FULL, Locale.ROOT);
Date lastDisconnect = dateFormat.parse(lastDisconnectedString);
playerData.lastDisconnect = lastDisconnect;
}
catch(Exception e)
{
playerData.lastDisconnect = Calendar.getInstance().getTime();
}
//parse priority string
if(rankString == null || rankString.isEmpty())
{
playerData.loginPriority = 0;
}
else
{
try
{
playerData.loginPriority = Integer.parseInt(rankString);
}
catch(Exception e)
{
playerData.loginPriority = 0;
}
}
//shove into memory for quick access
this.playerNameToPlayerDataMap.put(dest, playerData);
}
//if the file isn't found, just don't do anything (probably a new-to-server player)
catch(FileNotFoundException e)
{
return;
}
//if there's any problem with the file's content, log an error message and skip it
catch(Exception e)
{
PopulationDensity.AddLogEntry("Unable to load data for player \"" + source + "\": " + e.getMessage());
}
try
{
if(inStream != null) inStream.close();
}
catch(IOException exception){}
}
//adds a new region, assigning it a name and updating local variables accordingly
public RegionCoordinates addRegion()
{
//first, find a unique name for the new region
String newRegionName;
//select a name from the list of region names
//strategy: use names from the list in rotation, appending a number when a name is already used
//(redstone, mountain, valley, redstone1, mountain1, valley1, ...)
int newRegionNumber = this.coordsToNameMap.keySet().size() - 1;
//as long as the generated name is already in use, move up one name on the list
do
{
newRegionNumber++;
int nameBodyIndex = newRegionNumber % this.regionNamesList.length;
int nameSuffix = newRegionNumber / this.regionNamesList.length;
newRegionName = this.regionNamesList[nameBodyIndex];
if(nameSuffix > 0) newRegionName += nameSuffix;
}while(this.getRegionCoordinates(newRegionName) != null);
this.privateNameRegion(this.nextRegionCoordinates, newRegionName);
//find the next region in the spiral (updates this.openRegionCoordinates and this.nextRegionCoordinates)
this.findNextRegion();
return this.openRegionCoordinates;
}
//names a region, never throws an exception for name content
private void privateNameRegion(RegionCoordinates coords, String name)
{
//delete any existing data for the region at these coordinates
String oldRegionName = this.getRegionName(coords);
if(oldRegionName != null)
{
File oldRegionCoordinatesFile = new File(regionDataFolderPath + File.separator + coords.toString());
oldRegionCoordinatesFile.delete();
File oldRegionNameFile = new File(regionDataFolderPath + File.separator + oldRegionName);
oldRegionNameFile.delete();
this.coordsToNameMap.remove(coords);
this.nameToCoordsMap.remove(oldRegionName.toLowerCase());
}
//"create" the region by saving necessary data to disk
BufferedWriter outStream = null;
try
{
//coordinates file contains the region's name
File regionCoordinatesFile = new File(regionDataFolderPath + File.separator + coords.toString());
regionCoordinatesFile.createNewFile();
outStream = new BufferedWriter(new FileWriter(regionCoordinatesFile));
outStream.write(name);
outStream.close();
//cache in memory
this.coordsToNameMap.put(coords, name);
this.nameToCoordsMap.put(name.toLowerCase(), coords);
}
//in case of any problem, log the details
catch(Exception e)
{
PopulationDensity.AddLogEntry("Unexpected Exception: " + e.getMessage());
}
try
{
if(outStream != null) outStream.close();
}
catch(IOException exception){}
}
//names or renames a specified region
public void nameRegion(RegionCoordinates coords, String name) throws RegionNameException
{
//validate name
String error = PopulationDensity.instance.getRegionNameError(name, false);
if(error != null)
{
throw new RegionNameException(error);
}
this.privateNameRegion(coords, name);
}
//retrieves the open region's coordinates
public RegionCoordinates getOpenRegion()
{
return this.openRegionCoordinates;
}
//goes to disk to get the name of a region, given its coordinates
public String getRegionName(RegionCoordinates coordinates)
{
return this.coordsToNameMap.get(coordinates);
}
//similar to above, goes to disk to get the coordinates that go with a region name
public RegionCoordinates getRegionCoordinates(String regionName)
{
return this.nameToCoordsMap.get(regionName.toLowerCase());
}
//actually edits the world to create a region post at the center of the specified region
@SuppressWarnings("deprecation")
public void AddRegionPost(RegionCoordinates region) throws ChunkLoadException
{
//if region post building is disabled, don't do anything
if(!PopulationDensity.instance.buildRegionPosts) return;
//find the center
Location regionCenter = PopulationDensity.getRegionCenter(region, false);
int x = regionCenter.getBlockX();
int z = regionCenter.getBlockZ();
int y;
//make sure data is loaded for that area, because we're about to request data about specific blocks there
PopulationDensity.GuaranteeChunkLoaded(x, z);
//sink lower until we find something solid
//also ignore glowstone, in case there's already a post here!
Material blockType;
//find the highest block. could be the surface, a tree, some grass...
y = PopulationDensity.ManagedWorld.getHighestBlockYAt(x, z) + 1;
//posts fall through trees, snow, and any existing post looking for the ground
do
{
blockType = PopulationDensity.ManagedWorld.getBlockAt(x, --y, z).getType();
}
while( y > 0 && (
blockType == Material.AIR ||
blockType == Material.LEAVES ||
blockType == Material.LEAVES_2 ||
blockType == Material.LONG_GRASS||
blockType == Material.LOG ||
blockType == Material.LOG_2 ||
blockType == Material.SNOW ||
blockType == Material.VINE
));
if(blockType == Material.SIGN_POST)
{
y -= 4;
}
else if(blockType == Material.GLOWSTONE || (blockType == Material.getMaterial(PopulationDensity.instance.postTopperId)))
{
y -= 3;
}
else if(blockType == Material.BEDROCK)
{
y += 1;
}
//if y value is under sea level, correct it to sea level (no posts should be that difficult to find)
if(y < PopulationDensity.instance.minimumRegionPostY)
{
y = PopulationDensity.instance.minimumRegionPostY;
}
//clear signs from the area, this ensures signs don't drop as items
//when the blocks they're attached to are destroyed in the next step
for(int x1 = x - 2; x1 <= x + 2; x1++)
{
for(int z1 = z - 2; z1 <= z + 2; z1++)
{
for(int y1 = y + 1; y1 <= y + 5; y1++)
{
Block block = PopulationDensity.ManagedWorld.getBlockAt(x1, y1, z1);
if(block.getType() == Material.SIGN_POST || block.getType() == Material.SIGN || block.getType() == Material.WALL_SIGN)
block.setType(Material.AIR);
}
}
}
//clear above it - sometimes this shears trees in half (doh!)
for(int x1 = x - 2; x1 <= x + 2; x1++)
{
for(int z1 = z - 2; z1 <= z + 2; z1++)
{
for(int y1 = y + 1; y1 < y + 10; y1++)
{
Block block = PopulationDensity.ManagedWorld.getBlockAt(x1, y1, z1);
if(block.getType() != Material.AIR) block.setType(Material.AIR);
}
}
}
//build top block
PopulationDensity.ManagedWorld.getBlockAt(x, y + 3, z).setTypeIdAndData(PopulationDensity.instance.postTopperId, PopulationDensity.instance.postTopperData.byteValue(), true);
//build outer platform
for(int x1 = x - 2; x1 <= x + 2; x1++)
{
for(int z1 = z - 2; z1 <= z + 2; z1++)
{
PopulationDensity.ManagedWorld.getBlockAt(x1, y, z1).setTypeIdAndData(PopulationDensity.instance.outerPlatformId, PopulationDensity.instance.outerPlatformData.byteValue(), true);
}
}
//build inner platform
for(int x1 = x - 1; x1 <= x + 1; x1++)
{
for(int z1 = z - 1; z1 <= z + 1; z1++)
{
PopulationDensity.ManagedWorld.getBlockAt(x1, y, z1).setTypeIdAndData(PopulationDensity.instance.innerPlatformId, PopulationDensity.instance.innerPlatformData.byteValue(), true);
}
}
//build lower center blocks
for(int y1 = y; y1 <= y + 2; y1++)
{
PopulationDensity.ManagedWorld.getBlockAt(x, y1, z).setTypeIdAndData(PopulationDensity.instance.postId, PopulationDensity.instance.postData.byteValue(), true);
}
//build a sign on top with region name (or wilderness if no name)
String regionName = this.getRegionName(region);
if(regionName == null) regionName = "Wilderness";
regionName = PopulationDensity.capitalize(regionName);
Block block = PopulationDensity.ManagedWorld.getBlockAt(x, y + 4, z);
block.setType(Material.SIGN_POST);
org.bukkit.block.Sign sign = (org.bukkit.block.Sign)block.getState();
sign.setLine(1, PopulationDensity.capitalize(regionName));
sign.setLine(2, "Region");
sign.update();
//add a sign for the region to the south
regionName = this.getRegionName(new RegionCoordinates(region.x + 1, region.z));
if(regionName == null) regionName = "Wilderness";
regionName = PopulationDensity.capitalize(regionName);
block = PopulationDensity.ManagedWorld.getBlockAt(x, y + 2, z - 1);
org.bukkit.material.Sign signData = new org.bukkit.material.Sign(Material.WALL_SIGN);
signData.setFacingDirection(BlockFace.NORTH);
block.setTypeIdAndData(Material.WALL_SIGN.getId(), signData.getData(), false);
sign = (org.bukkit.block.Sign)block.getState();
sign.setLine(0, "<--");
sign.setLine(1, regionName);
sign.setLine(2, "Region");
sign.setLine(3, "<--");
sign.update();
//if a city world is defined, also add a /cityregion sign on the east side of the post
if(PopulationDensity.CityWorld != null)
{
block = PopulationDensity.ManagedWorld.getBlockAt(x, y + 3, z - 1);
//signData = new org.bukkit.material.Sign(Material.WALL_SIGN);
//signData.setFacingDirection(BlockFace.NORTH);
//block.setTypeIdAndData(Material.WALL_SIGN.getId(), signData.getData(), false);
//sign = (org.bukkit.block.Sign)block.getState();
//sign.update();
}
//add a sign for the region to the east
regionName = this.getRegionName(new RegionCoordinates(region.x, region.z - 1));
if(regionName == null) regionName = "Wilderness";
regionName = PopulationDensity.capitalize(regionName);
block = PopulationDensity.ManagedWorld.getBlockAt(x - 1, y + 2, z);
signData = new org.bukkit.material.Sign(Material.WALL_SIGN);
signData.setFacingDirection(BlockFace.WEST);
block.setTypeIdAndData(Material.WALL_SIGN.getId(), signData.getData(), false);
sign = (org.bukkit.block.Sign)block.getState();
sign.setLine(0, "<--");
sign.setLine(1, regionName);
sign.setLine(2, "Region");
sign.setLine(3, "<--");
sign.update();
//if teleportation is enabled, also add a sign facing north for teleportation help
if(PopulationDensity.instance.allowTeleportation)
{
block = PopulationDensity.ManagedWorld.getBlockAt(x - 1, y + 3, z);
signData = new org.bukkit.material.Sign(Material.WALL_SIGN);
signData.setFacingDirection(BlockFace.WEST);
block.setTypeIdAndData(Material.WALL_SIGN.getId(), signData.getData(), false);
sign = (org.bukkit.block.Sign)block.getState();
sign.setLine(0, "Teleport");
sign.setLine(1, "From Here!");
sign.setLine(2, "Punch For");
sign.setLine(3, "Instructions");
sign.update();
}
//add a sign for the region to the south
regionName = this.getRegionName(new RegionCoordinates(region.x, region.z + 1));
if(regionName == null) regionName = "Wilderness";
regionName = PopulationDensity.capitalize(regionName);
block = PopulationDensity.ManagedWorld.getBlockAt(x + 1, y + 2, z);
signData = new org.bukkit.material.Sign(Material.WALL_SIGN);
signData.setFacingDirection(BlockFace.EAST);
block.setTypeIdAndData(Material.WALL_SIGN.getId(), signData.getData(), false);
sign = (org.bukkit.block.Sign)block.getState();
sign.setLine(0, "<--");
sign.setLine(1, regionName);
sign.setLine(2, "Region");
sign.setLine(3, "<--");
sign.update();
//if teleportation is enabled, also add a sign facing south for teleportation help
if(PopulationDensity.instance.allowTeleportation)
{
block = PopulationDensity.ManagedWorld.getBlockAt(x + 1, y + 3, z);
signData = new org.bukkit.material.Sign(Material.WALL_SIGN);
signData.setFacingDirection(BlockFace.EAST);
block.setTypeIdAndData(Material.WALL_SIGN.getId(), signData.getData(), false);
sign = (org.bukkit.block.Sign)block.getState();
sign.setLine(0, "Teleport");
sign.setLine(1, "From Here!");
sign.setLine(2, "Punch For");
sign.setLine(3, "Instructions");
sign.update();
}
//add a sign for the region to the north
regionName = this.getRegionName(new RegionCoordinates(region.x - 1, region.z));
if(regionName == null) regionName = "Wilderness";
regionName = PopulationDensity.capitalize(regionName);
block = PopulationDensity.ManagedWorld.getBlockAt(x, y + 2, z + 1);
signData = new org.bukkit.material.Sign(Material.WALL_SIGN);
signData.setFacingDirection(BlockFace.SOUTH);
block.setTypeIdAndData(Material.WALL_SIGN.getId(), signData.getData(), false);
sign = (org.bukkit.block.Sign)block.getState();
sign.setLine(0, "<--");
sign.setLine(1, regionName);
sign.setLine(2, "Region");
sign.setLine(3, "<--");
sign.update();
//if teleportation is enabled, also add a sign facing west for /newestregion
if(PopulationDensity.instance.allowTeleportation && !this.openRegionCoordinates.equals(region))
{
//block = PopulationDensity.ManagedWorld.getBlockAt(x, y + 3, z + 1);
//signData = new org.bukkit.material.Sign(Material.WALL_SIGN);
//signData.setFacingDirection(BlockFace.SOUTH);
//block.setTypeIdAndData(Material.WALL_SIGN.getId(), signData.getData(), false);
//sign = (org.bukkit.block.Sign)block.getState();
//sign.update();
}
//custom signs
if(PopulationDensity.instance.mainCustomSignContent != null)
{
block = PopulationDensity.ManagedWorld.getBlockAt(x, y + 3, z - 1);
signData = new org.bukkit.material.Sign(Material.WALL_SIGN);
signData.setFacingDirection(BlockFace.NORTH);
block.setTypeIdAndData(Material.WALL_SIGN.getId(), signData.getData(), false);
sign = (org.bukkit.block.Sign)block.getState();
for(int i = 0; i < 4; i++)
{
sign.setLine(i, PopulationDensity.instance.mainCustomSignContent[i]);
}
sign.update();
}
if(PopulationDensity.instance.northCustomSignContent != null)
{
block = PopulationDensity.ManagedWorld.getBlockAt(x - 1, y + 1, z);
signData = new org.bukkit.material.Sign(Material.WALL_SIGN);
signData.setFacingDirection(BlockFace.WEST);
block.setTypeIdAndData(Material.WALL_SIGN.getId(), signData.getData(), false);
sign = (org.bukkit.block.Sign)block.getState();
for(int i = 0; i < 4; i++)
{
sign.setLine(i, PopulationDensity.instance.northCustomSignContent[i]);
}
sign.update();
}
if(PopulationDensity.instance.southCustomSignContent != null)
{
block = PopulationDensity.ManagedWorld.getBlockAt(x + 1, y + 1, z);
signData = new org.bukkit.material.Sign(Material.WALL_SIGN);
signData.setFacingDirection(BlockFace.EAST);
block.setTypeIdAndData(Material.WALL_SIGN.getId(), signData.getData(), false);
sign = (org.bukkit.block.Sign)block.getState();
for(int i = 0; i < 4; i++)
{
sign.setLine(i, PopulationDensity.instance.southCustomSignContent[i]);
}
sign.update();
}
if(PopulationDensity.instance.eastCustomSignContent != null)
{
block = PopulationDensity.ManagedWorld.getBlockAt(x, y + 1, z - 1);
signData = new org.bukkit.material.Sign(Material.WALL_SIGN);
signData.setFacingDirection(BlockFace.NORTH);
block.setTypeIdAndData(Material.WALL_SIGN.getId(), signData.getData(), false);
sign = (org.bukkit.block.Sign)block.getState();
for(int i = 0; i < 4; i++)
{
sign.setLine(i, PopulationDensity.instance.eastCustomSignContent[i]);
}
sign.update();
}
if(PopulationDensity.instance.westCustomSignContent != null)
{
block = PopulationDensity.ManagedWorld.getBlockAt(x, y + 1, z + 1);
signData = new org.bukkit.material.Sign(Material.WALL_SIGN);
signData.setFacingDirection(BlockFace.SOUTH);
block.setTypeIdAndData(Material.WALL_SIGN.getId(), signData.getData(), false);
sign = (org.bukkit.block.Sign)block.getState();
for(int i = 0; i < 4; i++)
{
sign.setLine(i, PopulationDensity.instance.westCustomSignContent[i]);
}
sign.update();
}
}
public void clearCachedPlayerData(Player player)
{
this.playerNameToPlayerDataMap.remove(player.getName());
}
private void loadMessages()
{
Messages [] messageIDs = Messages.values();
this.messages = new String[Messages.values().length];
HashMap<String, CustomizableMessage> defaults = new HashMap<String, CustomizableMessage>();
//initialize defaults
this.addDefault(defaults, Messages.NoManagedWorld, "The PopulationDensity plugin has not been properly configured. Please update your config.yml to specify a world to manage.", null);
this.addDefault(defaults, Messages.NoBreakPost, "You can't break blocks this close to the region post.", null);
this.addDefault(defaults, Messages.NoBreakSpawn, "You can't break blocks this close to a player spawn point.", null);
this.addDefault(defaults, Messages.NoBuildPost, "You can't place blocks this close to the region post.", null);
this.addDefault(defaults, Messages.NoBuildSpawn, "You can't place blocks this close to a player spawn point.", null);
this.addDefault(defaults, Messages.HelpMessage1, "Region post help and commands: {0} ", "0: Help URL");
this.addDefault(defaults, Messages.BuildingAwayFromHome, "You're building outside of your home region. If you'd like to make this region your new home to help you return here later, use /MoveIn.", null);
this.addDefault(defaults, Messages.NoTeleportThisWorld, "You can't teleport from this world.", null);
this.addDefault(defaults, Messages.OnlyHomeCityHere, "You're limited to /HomeRegion and /CityRegion here.", null);
this.addDefault(defaults, Messages.NoTeleportHere, "Sorry, you can't teleport from here.", null);
this.addDefault(defaults, Messages.NotCloseToPost, "You're not close enough to a region post to teleport.", null);
this.addDefault(defaults, Messages.InvitationNeeded, "{0} lives in the wilderness. He or she will have to /invite you.", "0: target player");
this.addDefault(defaults, Messages.VisitConfirmation, "Teleported to {0}'s home region.", "0: target player");
this.addDefault(defaults, Messages.DestinationNotFound, "There's no region or online player named \"{0}\". Use /ListRegions to list possible destinations.", "0: specified destination");
this.addDefault(defaults, Messages.NeedNewestRegionPermission, "You don't have permission to use that command.", null);
this.addDefault(defaults, Messages.NewestRegionConfirmation, "Teleported to the current new player area.", null);
this.addDefault(defaults, Messages.NotInRegion, "You're not in a region!", null);
this.addDefault(defaults, Messages.UnnamedRegion, "You're in the wilderness! This region doesn't have a name.", null);
this.addDefault(defaults, Messages.WhichRegion, "You're in the {0} region.", null);
this.addDefault(defaults, Messages.RegionNamesNoSpaces, "Region names may not include spaces.", null);
this.addDefault(defaults, Messages.RegionNameLength, "Region names must be at most {0} letters long.", "0: maximum length specified in config.yml");
this.addDefault(defaults, Messages.RegionNamesOnlyLettersAndNumbers, "Region names may not include symbols or punctuation.", null);
this.addDefault(defaults, Messages.RegionNameConflict, "There's already a region by that name.", null);
this.addDefault(defaults, Messages.NoMoreRegions, "Sorry, you're in the only region. Over time, more regions will open.", null);
this.addDefault(defaults, Messages.InviteConfirmation, "{0} may now use /visit {1} to teleport to your home post.", "0: invitee's name, 1: inviter's name");
this.addDefault(defaults, Messages.PlayerNotFound, "There's no player named \"{0}\" online right now.", "0: specified name");
this.addDefault(defaults, Messages.SetHomeConfirmation, "Home set to the nearest region post!", null);
this.addDefault(defaults, Messages.SetHomeInstruction1, "Use /Home from any region post to teleport to your home post.", null);
this.addDefault(defaults, Messages.SetHomeInstruction2, "Use /Invite to invite other players to teleport to your home post.", null);
this.addDefault(defaults, Messages.AddRegionConfirmation, "Opened a new region and started a resource scan. See console or server logs for details.", null);
this.addDefault(defaults, Messages.ScanStartConfirmation, "Started scan. Check console or server logs for results.", null);
this.addDefault(defaults, Messages.LoginPriorityCheck, "{0}'s login priority: {1}.", "0: player name, 1: current priority");
this.addDefault(defaults, Messages.LoginPriorityUpdate, "Set {0}'s priority to {1}.", "0: target player, 1: new priority");
this.addDefault(defaults, Messages.ThinningConfirmation, "Thinning running. Check logs for detailed results.", null);
this.addDefault(defaults, Messages.PerformanceScore, "Current server performance score is {0}%.", "0: performance score");
this.addDefault(defaults, Messages.PerformanceScore_Lag, " The server is actively working to reduce lag - please be patient while automatic lag reduction takes effect.", null);
this.addDefault(defaults, Messages.PerformanceScore_NoLag, "The server is running at normal speed. If you're experiencing lag, check your graphics settings and internet connection. ", null);
this.addDefault(defaults, Messages.PlayerMoved, "Player moved.", null);
this.addDefault(defaults, Messages.Lag, "lag", null);
this.addDefault(defaults, Messages.RegionAlreadyNamed, "This region already has a name. To REname, use /RenameRegion.", null);
this.addDefault(defaults, Messages.HopperLimitReached, "To prevent server lag, hoppers are limited to {0} per chunk.", "0: maximum hoppers per chunk");
//load the config file
FileConfiguration config = YamlConfiguration.loadConfiguration(new File(messagesFilePath));
//for each message ID
for(int i = 0; i < messageIDs.length; i++)
{
//get default for this message
Messages messageID = messageIDs[i];
CustomizableMessage messageData = defaults.get(messageID.name());
//if default is missing, log an error and use some fake data for now so that the plugin can run
if(messageData == null)
{
PopulationDensity.AddLogEntry("Missing message for " + messageID.name() + ". Please contact the developer.");
messageData = new CustomizableMessage(messageID, "Missing message! ID: " + messageID.name() + ". Please contact a server admin.", null);
}
//read the message from the file, use default if necessary
this.messages[messageID.ordinal()] = config.getString("Messages." + messageID.name() + ".Text", messageData.text);
config.set("Messages." + messageID.name() + ".Text", this.messages[messageID.ordinal()]);
if(messageData.notes != null)
{
messageData.notes = config.getString("Messages." + messageID.name() + ".Notes", messageData.notes);
config.set("Messages." + messageID.name() + ".Notes", messageData.notes);
}
}
//save any changes
try
{
config.save(DataStore.messagesFilePath);
}
catch(IOException exception)
{
PopulationDensity.AddLogEntry("Unable to write to the configuration file at \"" + DataStore.messagesFilePath + "\"");
}
defaults.clear();
System.gc();
}
private void addDefault(HashMap<String, CustomizableMessage> defaults, Messages id, String text, String notes)
{
CustomizableMessage message = new CustomizableMessage(id, text, notes);
defaults.put(id.name(), message);
}
synchronized public String getMessage(Messages messageID, String... args)
{
String message = messages[messageID.ordinal()];
for(int i = 0; i < args.length; i++)
{
String param = args[i];
message = message.replace("{" + i + "}", param);
}
return message;
}
//list of region names to use
private String [] regionNamesList;
String getRegionNames()
{
StringBuilder builder = new StringBuilder();
for(String regionName : this.nameToCoordsMap.keySet())
{
builder.append(PopulationDensity.capitalize(regionName)).append(", ");
}
return builder.toString();
}
@Override
public List<String> onTabComplete(CommandSender sender, Command command, String alias, String[] args) throws IllegalArgumentException
{
Validate.notNull(sender, "Sender cannot be null");
Validate.notNull(args, "Arguments cannot be null");
Validate.notNull(alias, "Alias cannot be null");
if (args.length == 0)
{
return ImmutableList.of();
}
StringBuilder builder = new StringBuilder();
for(String arg : args)
{
builder.append(arg + " ");
}
String arg = builder.toString().trim();
ArrayList<String> matches = new ArrayList<String>();
for (String name : this.coordsToNameMap.values())
{
if (StringUtil.startsWithIgnoreCase(name, arg))
{
matches.add(name);
}
}
Player senderPlayer = sender instanceof Player ? (Player) sender : null;
for(Player player : sender.getServer().getOnlinePlayers())
{
if(senderPlayer == null || senderPlayer.canSee(player))
{
if(StringUtil.startsWithIgnoreCase(player.getName(), arg))
{
matches.add(player.getName());
}
}
}
Collections.sort(matches, String.CASE_INSENSITIVE_ORDER);
return matches;
}
}