diff --git a/src/com/massivecraft/mcore4/MCore.java b/src/com/massivecraft/mcore4/MCore.java index 3635c1d7..4a6e3f7f 100644 --- a/src/com/massivecraft/mcore4/MCore.java +++ b/src/com/massivecraft/mcore4/MCore.java @@ -31,7 +31,8 @@ public class MCore extends JavaPlugin { logPrefix = "["+this.getDescription().getName()+"] "; - PlayerUtil.populateAllVisitorNames(); + // Setup PlayerUtil and it's events + new PlayerUtil(this); // This is safe since all plugins using Persist should bukkit-depend this plugin. Persist.instances.clear(); diff --git a/src/com/massivecraft/mcore4/util/PlayerUtil.java b/src/com/massivecraft/mcore4/util/PlayerUtil.java index 1a637815..8666c66b 100644 --- a/src/com/massivecraft/mcore4/util/PlayerUtil.java +++ b/src/com/massivecraft/mcore4/util/PlayerUtil.java @@ -2,8 +2,12 @@ package com.massivecraft.mcore4.util; import java.io.File; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.TreeSet; +import java.util.Map.Entry; +import java.util.concurrent.ConcurrentSkipListMap; +import java.util.regex.Pattern; import net.minecraft.server.DedicatedServer; import net.minecraft.server.EntityPlayer; @@ -13,36 +17,78 @@ import org.bukkit.Bukkit; import org.bukkit.craftbukkit.CraftServer; import org.bukkit.craftbukkit.entity.CraftPlayer; import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerPreLoginEvent; +import org.bukkit.plugin.Plugin; -public class PlayerUtil +public class PlayerUtil implements Listener { - private static Set allVisitorNames = new TreeSet(String.CASE_INSENSITIVE_ORDER); - public static Set getAllVisitorNames() { return allVisitorNames; } - public static void populateAllVisitorNames() + // -------------------------------------------- // + // FIELDS + // -------------------------------------------- // + + /** + * This is the latest created instance of this class. + */ + private static PlayerUtil i = null; + + /** + * We will use this folder later. + */ + public static File playerfolder = getPlayerFolder(); + + /** + * This map is populated using the player.dat files on disk. + * It is also populated when a player tries to log in to the server. + */ + protected static Map nameToCorrectName = new ConcurrentSkipListMap(String.CASE_INSENSITIVE_ORDER); + + /** + * This map is used to improve the speed of name start lookups. + * Note that the keys in this map is lowercase. + */ + protected static Map> lowerCaseStartOfNameToCorrectNames = new ConcurrentSkipListMap>(); + + // -------------------------------------------- // + // CONSTRUCTOR AND EVENT LISTENER + // -------------------------------------------- // + + public PlayerUtil(Plugin plugin) { - // Find the player folder - CraftServer cserver = (CraftServer)Bukkit.getServer(); - DedicatedServer dserver = (DedicatedServer)cserver.getServer(); - String levelName = dserver.propertyManager.getString("level-name", "world"); - File playerfolder = new File(Bukkit.getWorldContainer(), new File(levelName, "players").getPath()); + if (i != null) return; + i = this; + Bukkit.getPluginManager().registerEvents(this, plugin); + populateCaseInsensitiveNameToCaseCorrectName(); + } + + @EventHandler(priority = EventPriority.LOWEST) + public void onLowestPlayerPreLoginEvent(PlayerPreLoginEvent event) + { + String newPlayerName = event.getName(); + String lowercaseNewPlayerName = newPlayerName.toLowerCase(); - // Populate by removing .dat - for (File playerfile : playerfolder.listFiles()) + // Add this name to the case-corrector map + nameToCorrectName.put(newPlayerName, newPlayerName); + + // Update the cache + for (Entry> entry : lowerCaseStartOfNameToCorrectNames.entrySet()) { - String filename = playerfile.getName(); - String playername = filename.substring(0, filename.length()-4); - allVisitorNames.add(playername); + if (lowercaseNewPlayerName.startsWith(entry.getKey())) + { + entry.getValue().add(newPlayerName); + } } } - public static void sendHealthFoodUpdatePacket(Player player) - { - CraftPlayer cplayer = (CraftPlayer)player; - EntityPlayer eplayer = cplayer.getHandle(); - eplayer.netServerHandler.sendPacket(new Packet8UpdateHealth(eplayer.getHealth(), eplayer.getFoodData().a(), eplayer.getFoodData().e())); - } + // -------------------------------------------- // + // PUBLIC METHODS + // -------------------------------------------- // - // TODO: Is there synchronization/parallelism risks here? + /** + * This is a faster version of the getPlayerExact method since this one is exact for real (no to lower case stuff). + */ @SuppressWarnings("unchecked") public static Player getPlayerExact(String exactPlayerName) { @@ -55,7 +101,154 @@ public class PlayerUtil { return player; } - } + } return null; } + + /** + * This method simply checks if the playerName is a valid one. + * Mojangs rules for Minecraft character registration is used. + */ + public static boolean isValidPlayerName(final String playerName) + { + return Pattern.matches("^[a-zA-Z0-9_]{2,16}$", playerName); + } + + public static Set getAllVisitorNames() + { + return nameToCorrectName.keySet(); + } + + /** + * This method takes a player name and returns the same name but with correct case. + * Null is returned if the correct case can not be determined. + */ + public static String fixPlayerNameCase(final String playerName) + { + return nameToCorrectName.get(playerName); + } + + /** + * Find all player names starting with a certain string (not case sensitive). + * This method will return the names of offline players as well as online players. + */ + public static Set getAllPlayerNamesCaseinsensitivelyStartingWith(final String startOfName) + { + Set ret = new TreeSet(String.CASE_INSENSITIVE_ORDER); + + String lowercaseStartOfName = startOfName.toLowerCase(); + + // Try to fetch from the cache + Set cachedNames = lowerCaseStartOfNameToCorrectNames.get(lowercaseStartOfName); + if (cachedNames != null) + { + ret.addAll(cachedNames); + return ret; + } + + // Build it the hard way if cache did not exist + + ret = new TreeSet(String.CASE_INSENSITIVE_ORDER); + for (String correctName : nameToCorrectName.values()) + { + if (correctName.toLowerCase().startsWith(lowercaseStartOfName)) + { + ret.add(correctName); + } + } + + // Add it to the cache + Set shallowCopyForCache = new TreeSet(String.CASE_INSENSITIVE_ORDER); + shallowCopyForCache.addAll(ret); + lowerCaseStartOfNameToCorrectNames.put(lowercaseStartOfName, shallowCopyForCache); + + return ret; + } + + /** + * In Minecraft a playername can be 16 characters long. One sign line is however only 15 characters long. + * If we find a 15 character long playername on a sign it could thus refer to more than one player. + * This method finds all possible matching player names. + */ + public static Set interpretPlayerNameFromSign(String playerNameFromSign) + { + Set ret = new TreeSet(String.CASE_INSENSITIVE_ORDER); + + if (playerNameFromSign.length() > 15) + { + // This case will in reality not happen. + ret.add(playerNameFromSign); + return ret; + } + + if (playerNameFromSign.length() == 15) + { + ret.addAll(PlayerUtil.getAllPlayerNamesCaseinsensitivelyStartingWith(playerNameFromSign)); + } + else + { + String fixedPlayerName = PlayerUtil.fixPlayerNameCase(playerNameFromSign); + if (fixedPlayerName != null) + { + ret.add(fixedPlayerName); + } + } + + return ret; + } + + /** + * It seems the OfflinePlayer#getLastPlayed in Bukkit is broken. + * It occasionally returns invalid values. Therefore we use this instead. + * The playerName must be the full name but is not case sensitive. + */ + public static long getLastPlayed(String playerName) + { + String playerNameCC = fixPlayerNameCase(playerName); + if (playerNameCC == null) return 0; + + Player player = Bukkit.getPlayerExact(playerNameCC); + if (player != null && player.isOnline()) return System.currentTimeMillis(); + + File playerFile = new File(playerfolder, playerNameCC+".dat"); + return playerFile.lastModified(); + } + + /** + * Updates the players food and health information. + */ + public static void sendHealthFoodUpdatePacket(Player player) + { + CraftPlayer cplayer = (CraftPlayer)player; + EntityPlayer eplayer = cplayer.getHandle(); + eplayer.netServerHandler.sendPacket(new Packet8UpdateHealth(eplayer.getHealth(), eplayer.getFoodData().a(), eplayer.getFoodData().e())); + } + + // -------------------------------------------- // + // INTERNAL METHODS + // -------------------------------------------- // + + protected static void populateCaseInsensitiveNameToCaseCorrectName() + { + // Populate by removing .dat + for (File playerfile : playerfolder.listFiles()) + { + String filename = playerfile.getName(); + String playername = filename.substring(0, filename.length()-4); + nameToCorrectName.put(playername, playername); + } + } + + /** + * You might ask yourself why we do this in such a low-level way. + * The reason is this info is not yet "compiled" for plugins that init early. + */ + protected static File getPlayerFolder() + { + CraftServer cserver = (CraftServer)Bukkit.getServer(); + DedicatedServer dserver = (DedicatedServer)cserver.getServer(); + String levelName = dserver.propertyManager.getString("level-name", "world"); + return new File(Bukkit.getWorldContainer(), new File(levelName, "players").getPath()); + } + }