From 2984d9513f41f34c0346bfeaa489371cf7db89a2 Mon Sep 17 00:00:00 2001 From: Brettflan Date: Wed, 28 May 2014 11:13:16 -0500 Subject: [PATCH] Changed bypass list to track UUIDs instead of names, in advance of changeable names in Minecraft 1.8.0+. There might be a slight delay in response now when running the /wb bypasslist command, and when running the /wb bypass command on a player who is not online. This is because the Mojang UUID lookup server must be queried in those cases. This may be handled better in a future update if/when Bukkit provides methods for caching and looking up that information. The UUID/player name lookup code I've used is by evilmidget38, from: http://forums.bukkit.org/threads/player-name-uuid-fetcher.250926/ NOTE: if you have a bypass list saved, it will be wiped when you first run this update. The wipe will only happen once, as it is a safety precaution. --- pom.xml | 2 +- .../wimbli/WorldBorder/BorderCheckTask.java | 2 +- .../java/com/wimbli/WorldBorder/Config.java | 50 ++++++--- .../wimbli/WorldBorder/UUID/NameFetcher.java | 49 +++++++++ .../wimbli/WorldBorder/UUID/UUIDFetcher.java | 103 ++++++++++++++++++ .../com/wimbli/WorldBorder/cmd/CmdBypass.java | 71 +++++++++--- .../wimbli/WorldBorder/cmd/CmdBypasslist.java | 35 +++++- 7 files changed, 275 insertions(+), 37 deletions(-) create mode 100644 src/main/java/com/wimbli/WorldBorder/UUID/NameFetcher.java create mode 100644 src/main/java/com/wimbli/WorldBorder/UUID/UUIDFetcher.java diff --git a/pom.xml b/pom.xml index be27cf8..87258b4 100644 --- a/pom.xml +++ b/pom.xml @@ -30,7 +30,7 @@ org.bukkit bukkit - 1.7.2-R0.4-SNAPSHOT + 1.7.9-R0.1 org.dynmap diff --git a/src/main/java/com/wimbli/WorldBorder/BorderCheckTask.java b/src/main/java/com/wimbli/WorldBorder/BorderCheckTask.java index 975ad71..cadd446 100644 --- a/src/main/java/com/wimbli/WorldBorder/BorderCheckTask.java +++ b/src/main/java/com/wimbli/WorldBorder/BorderCheckTask.java @@ -53,7 +53,7 @@ public class BorderCheckTask implements Runnable return null; // if player is in bypass list (from bypass command), allow them beyond border; also ignore players currently being handled already - if (Config.isPlayerBypassing(player.getName()) || handlingPlayers.contains(player.getName().toLowerCase())) + if (Config.isPlayerBypassing(player.getUniqueId()) || handlingPlayers.contains(player.getName().toLowerCase())) return null; // tag this player as being handled so we can't get stuck in a loop due to Bukkit currently sometimes repeatedly providing incorrect location through teleport event diff --git a/src/main/java/com/wimbli/WorldBorder/Config.java b/src/main/java/com/wimbli/WorldBorder/Config.java index c1b876a..f340aa0 100644 --- a/src/main/java/com/wimbli/WorldBorder/Config.java +++ b/src/main/java/com/wimbli/WorldBorder/Config.java @@ -6,11 +6,13 @@ import java.util.Collections; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.LinkedHashSet; +import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; import java.util.Map; import java.util.Map.Entry; import java.util.Set; +import java.util.UUID; import org.bukkit.configuration.ConfigurationSection; import org.bukkit.configuration.file.FileConfiguration; @@ -36,7 +38,7 @@ public class Config // actual configuration values which can be changed private static boolean shapeRound = true; private static Map borders = Collections.synchronizedMap(new LinkedHashMap()); - private static Set bypassPlayers = Collections.synchronizedSet(new LinkedHashSet()); + private static Set bypassPlayers = Collections.synchronizedSet(new LinkedHashSet()); private static String message; // raw message without color code formatting private static String messageFmt; // message with color code formatting ("&" changed to funky sort-of-double-dollar-sign for legitimate color/formatting codes) private static String messageClean; // message cleaned of formatting codes @@ -373,32 +375,42 @@ public class Config return dynmapMessage; } - public static void setPlayerBypass(String player, boolean bypass) + public static void setPlayerBypass(UUID player, boolean bypass) { if (bypass) - bypassPlayers.add(player.toLowerCase()); + bypassPlayers.add(player); else - bypassPlayers.remove(player.toLowerCase()); + bypassPlayers.remove(player); + save(true); } - public static boolean isPlayerBypassing(String player) + public static boolean isPlayerBypassing(UUID player) { - return bypassPlayers.contains(player.toLowerCase()); + return bypassPlayers.contains(player); } - public static void togglePlayerBypass(String player) + public static ArrayList getPlayerBypassList() { - setPlayerBypass(player, !isPlayerBypassing(player)); + return new ArrayList(bypassPlayers); } - public static String getPlayerBypassList() + // for converting bypass UUID list to/from String list, for storage in config + private static void importBypassStringList(List strings) { - if (bypassPlayers.isEmpty()) - return ""; - String newString = bypassPlayers.toString(); - return newString.substring(1, newString.length() - 1); + for (String string: strings) + { + bypassPlayers.add(UUID.fromString(string)); + } + } + private static ArrayList exportBypassStringList() + { + ArrayList strings = new ArrayList(); + for (UUID uuid: bypassPlayers) + { + strings.add(uuid.toString()); + } + return strings; } - public static boolean isBorderTimerRunning() @@ -526,7 +538,7 @@ public class Config } - private static final int currentCfgVersion = 10; + private static final int currentCfgVersion = 11; public static void load(WorldBorder master, boolean logIt) { // load config from file @@ -552,7 +564,7 @@ public class Config killPlayer = cfg.getBoolean("player-killed-bad-spawn", false); denyEnderpearl = cfg.getBoolean("deny-enderpearl", true); fillAutosaveFrequency = cfg.getInt("fill-autosave-frequency", 30); - bypassPlayers = Collections.synchronizedSet(new LinkedHashSet(cfg.getStringList("bypass-list"))); + importBypassStringList(cfg.getStringList("bypass-list-uuids")); fillMemoryTolerance = cfg.getInt("fill-memory-tolerance", 500); StartBorderTimer(); @@ -579,6 +591,10 @@ public class Config if (cfgVersion < 10) denyEnderpearl = true; + // the border bypass list used to be stored as list of names rather than UUIDs; wipe that old list so the data won't be automatically saved back to the config file again + if (cfgVersion < 11) + cfg.set("bypass-list", null); + ConfigurationSection worlds = cfg.getConfigurationSection("worlds"); if (worlds != null) { @@ -654,7 +670,7 @@ public class Config cfg.set("player-killed-bad-spawn", killPlayer); cfg.set("deny-enderpearl", denyEnderpearl); cfg.set("fill-autosave-frequency", fillAutosaveFrequency); - cfg.set("bypass-list", new ArrayList(bypassPlayers)); + cfg.set("bypass-list-uuids", exportBypassStringList()); cfg.set("fill-memory-tolerance", fillMemoryTolerance); cfg.set("worlds", null); diff --git a/src/main/java/com/wimbli/WorldBorder/UUID/NameFetcher.java b/src/main/java/com/wimbli/WorldBorder/UUID/NameFetcher.java new file mode 100644 index 0000000..e1372c7 --- /dev/null +++ b/src/main/java/com/wimbli/WorldBorder/UUID/NameFetcher.java @@ -0,0 +1,49 @@ +package com.wimbli.WorldBorder.UUID; + +import com.google.common.collect.ImmutableList; +import org.json.simple.JSONObject; +import org.json.simple.parser.JSONParser; + +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.Callable; + + +/* + * code by evilmidget38 + * from http://forums.bukkit.org/threads/player-name-uuid-fetcher.250926/ + */ + +public class NameFetcher implements Callable> { + private static final String PROFILE_URL = "https://sessionserver.mojang.com/session/minecraft/profile/"; + private final JSONParser jsonParser = new JSONParser(); + private final List uuids; + public NameFetcher(List uuids) { + this.uuids = ImmutableList.copyOf(uuids); + } + + @Override + public Map call() throws Exception { + Map uuidStringMap = new HashMap(); + for (UUID uuid: uuids) { + HttpURLConnection connection = (HttpURLConnection) new URL(PROFILE_URL+uuid.toString().replace("-", "")).openConnection(); + JSONObject response = (JSONObject) jsonParser.parse(new InputStreamReader(connection.getInputStream())); + String name = (String) response.get("name"); + if (name == null) { + continue; + } + String cause = (String) response.get("cause"); + String errorMessage = (String) response.get("errorMessage"); + if (cause != null && cause.length() > 0) { + throw new IllegalStateException(errorMessage); + } + uuidStringMap.put(uuid, name); + } + return uuidStringMap; + } +} \ No newline at end of file diff --git a/src/main/java/com/wimbli/WorldBorder/UUID/UUIDFetcher.java b/src/main/java/com/wimbli/WorldBorder/UUID/UUIDFetcher.java new file mode 100644 index 0000000..d7455ca --- /dev/null +++ b/src/main/java/com/wimbli/WorldBorder/UUID/UUIDFetcher.java @@ -0,0 +1,103 @@ +package com.wimbli.WorldBorder.UUID; + +import com.google.common.collect.ImmutableList; +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; +import org.json.simple.parser.JSONParser; + +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.ByteBuffer; +import java.util.*; +import java.util.concurrent.Callable; + + +/* + * code by evilmidget38 + * from http://forums.bukkit.org/threads/player-name-uuid-fetcher.250926/ + * slightly modified to fix name case mismatches for single name lookup + */ + +public class UUIDFetcher implements Callable> { + private static final double PROFILES_PER_REQUEST = 100; + private static final String PROFILE_URL = "https://api.mojang.com/profiles/minecraft"; + private final JSONParser jsonParser = new JSONParser(); + private final List names; + private final boolean rateLimiting; + + public UUIDFetcher(List names, boolean rateLimiting) { + this.names = ImmutableList.copyOf(names); + this.rateLimiting = rateLimiting; + } + + public UUIDFetcher(List names) { + this(names, true); + } + + public Map call() throws Exception { + Map uuidMap = new HashMap(); + int requests = (int) Math.ceil(names.size() / PROFILES_PER_REQUEST); + for (int i = 0; i < requests; i++) { + HttpURLConnection connection = createConnection(); + String body = JSONArray.toJSONString(names.subList(i * 100, Math.min((i + 1) * 100, names.size()))); + writeBody(connection, body); + JSONArray array = (JSONArray) jsonParser.parse(new InputStreamReader(connection.getInputStream())); + for (Object profile : array) { + JSONObject jsonProfile = (JSONObject) profile; + String id = (String) jsonProfile.get("id"); + String name = (String) jsonProfile.get("name"); + UUID uuid = UUIDFetcher.getUUID(id); + uuidMap.put(name.toLowerCase(), uuid); + } + if (rateLimiting && i != requests - 1) { + Thread.sleep(100L); + } + } + return uuidMap; + } + + private static void writeBody(HttpURLConnection connection, String body) throws Exception { + OutputStream stream = connection.getOutputStream(); + stream.write(body.getBytes()); + stream.flush(); + stream.close(); + } + + private static HttpURLConnection createConnection() throws Exception { + URL url = new URL(PROFILE_URL); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("POST"); + connection.setRequestProperty("Content-Type", "application/json"); + connection.setUseCaches(false); + connection.setDoInput(true); + connection.setDoOutput(true); + return connection; + } + + private static UUID getUUID(String id) { + return UUID.fromString(id.substring(0, 8) + "-" + id.substring(8, 12) + "-" + id.substring(12, 16) + "-" + id.substring(16, 20) + "-" +id.substring(20, 32)); + } + + public static byte[] toBytes(UUID uuid) { + ByteBuffer byteBuffer = ByteBuffer.wrap(new byte[16]); + byteBuffer.putLong(uuid.getMostSignificantBits()); + byteBuffer.putLong(uuid.getLeastSignificantBits()); + return byteBuffer.array(); + } + + public static UUID fromBytes(byte[] array) { + if (array.length != 16) { + throw new IllegalArgumentException("Illegal byte array length: " + array.length); + } + ByteBuffer byteBuffer = ByteBuffer.wrap(array); + long mostSignificant = byteBuffer.getLong(); + long leastSignificant = byteBuffer.getLong(); + return new UUID(mostSignificant, leastSignificant); + } + + public static UUID getUUIDOf(String name) throws Exception { + return new UUIDFetcher(Arrays.asList(name)).call().get(name.toLowerCase()); + } +} diff --git a/src/main/java/com/wimbli/WorldBorder/cmd/CmdBypass.java b/src/main/java/com/wimbli/WorldBorder/cmd/CmdBypass.java index 1b3a4bb..fec9b35 100644 --- a/src/main/java/com/wimbli/WorldBorder/cmd/CmdBypass.java +++ b/src/main/java/com/wimbli/WorldBorder/cmd/CmdBypass.java @@ -1,12 +1,14 @@ package com.wimbli.WorldBorder.cmd; import java.util.List; +import java.util.UUID; import org.bukkit.Bukkit; import org.bukkit.command.*; import org.bukkit.entity.Player; import com.wimbli.WorldBorder.*; +import com.wimbli.WorldBorder.UUID.UUIDFetcher; public class CmdBypass extends WBCmd @@ -27,13 +29,15 @@ public class CmdBypass extends WBCmd @Override public void cmdStatus(CommandSender sender) { - boolean bypass = Config.isPlayerBypassing(((Player)sender).getName()); - if (sender instanceof Player) - sender.sendMessage(C_HEAD + "Border bypass is currently " + enabledColored(bypass) + C_HEAD + " for you."); + if (!(sender instanceof Player)) + return; + + boolean bypass = Config.isPlayerBypassing(((Player)sender).getUniqueId()); + sender.sendMessage(C_HEAD + "Border bypass is currently " + enabledColored(bypass) + C_HEAD + " for you."); } @Override - public void execute(CommandSender sender, Player player, List params, String worldName) + public void execute(final CommandSender sender, final Player player, final List params, String worldName) { if (player == null && params.isEmpty()) { @@ -41,21 +45,56 @@ public class CmdBypass extends WBCmd return; } - String sPlayer = (params.isEmpty()) ? player.getName() : params.get(0); + Bukkit.getServer().getScheduler().scheduleSyncDelayedTask(WorldBorder.plugin, new Runnable() + { + @Override + public void run() + { + final String sPlayer = (params.isEmpty()) ? player.getName() : params.get(0); + UUID uPlayer = (params.isEmpty()) ? player.getUniqueId() : null; - boolean bypassing = !Config.isPlayerBypassing(sPlayer); - if (params.size() > 1) - bypassing = strAsBool(params.get(1)); + if (uPlayer == null) + { + Player p = Bukkit.getPlayer(sPlayer); + if (p != null) + { + uPlayer = p.getUniqueId(); + } + else + { + // only do UUID lookup using Mojang server if specified player isn't online + try + { + uPlayer = UUIDFetcher.getUUIDOf(sPlayer); + } + catch(Exception ex) + { + sendErrorAndHelp(sender, "Failed to look up UUID for the player name you specified. " + ex.getLocalizedMessage()); + return; + } + } + } + if (uPlayer == null) + { + sendErrorAndHelp(sender, "Failed to look up UUID for the player name you specified; null value returned."); + return; + } - Config.setPlayerBypass(sPlayer, bypassing); + boolean bypassing = !Config.isPlayerBypassing(uPlayer); + if (params.size() > 1) + bypassing = strAsBool(params.get(1)); - Player target = Bukkit.getPlayer(sPlayer); - if (target != null && target.isOnline()) - target.sendMessage("Border bypass is now " + enabledColored(bypassing) + "."); + Config.setPlayerBypass(uPlayer, bypassing); - Config.log("Border bypass for player \"" + sPlayer + "\" is " + (bypassing ? "enabled" : "disabled") + - (player != null ? " at the command of player \"" + player.getName() + "\"" : "") + "."); - if (player != null && player != target) - sender.sendMessage("Border bypass for player \"" + sPlayer + "\" is " + enabledColored(bypassing) + "."); + Player target = Bukkit.getPlayer(sPlayer); + if (target != null && target.isOnline()) + target.sendMessage("Border bypass is now " + enabledColored(bypassing) + "."); + + Config.log("Border bypass for player \"" + sPlayer + "\" is " + (bypassing ? "enabled" : "disabled") + + (player != null ? " at the command of player \"" + player.getName() + "\"" : "") + "."); + if (player != null && player != target) + sender.sendMessage("Border bypass for player \"" + sPlayer + "\" is " + enabledColored(bypassing) + "."); + } + }); } } diff --git a/src/main/java/com/wimbli/WorldBorder/cmd/CmdBypasslist.java b/src/main/java/com/wimbli/WorldBorder/cmd/CmdBypasslist.java index dbea75a..3181160 100644 --- a/src/main/java/com/wimbli/WorldBorder/cmd/CmdBypasslist.java +++ b/src/main/java/com/wimbli/WorldBorder/cmd/CmdBypasslist.java @@ -1,11 +1,16 @@ package com.wimbli.WorldBorder.cmd; +import java.util.ArrayList; import java.util.List; +import java.util.Map; +import java.util.UUID; +import org.bukkit.Bukkit; import org.bukkit.command.*; import org.bukkit.entity.Player; import com.wimbli.WorldBorder.*; +import com.wimbli.WorldBorder.UUID.NameFetcher; public class CmdBypasslist extends WBCmd @@ -21,8 +26,34 @@ public class CmdBypasslist extends WBCmd } @Override - public void execute(CommandSender sender, Player player, List params, String worldName) + public void execute(final CommandSender sender, Player player, List params, String worldName) { - sender.sendMessage("Players with border bypass enabled: " + Config.getPlayerBypassList()); + final ArrayList uuids = Config.getPlayerBypassList(); + if (uuids == null || uuids.isEmpty()) + { + sender.sendMessage("Players with border bypass enabled: "); + return; + } + + Bukkit.getServer().getScheduler().scheduleSyncDelayedTask(WorldBorder.plugin, new Runnable() + { + @Override + public void run() + { + try + { + NameFetcher fetcher = new NameFetcher(uuids); + Map names = fetcher.call(); + String nameString = names.values().toString(); + + sender.sendMessage("Players with border bypass enabled: " + nameString.substring(1, nameString.length() - 1)); + } + catch(Exception ex) + { + sendErrorAndHelp(sender, "Failed to look up names for the UUIDs in the border bypass list. " + ex.getLocalizedMessage()); + return; + } + } + }); } }