From e14d52654152013de80624c65a7f05bd222b335e Mon Sep 17 00:00:00 2001 From: Brettflan Date: Thu, 23 May 2019 19:48:54 -0500 Subject: [PATCH] Misc 1.14 updates: * using "force loaded" chunk flag instead of canceling ChunkUnloadEvents, which is no longer possible as of Spigot 1.14 * replaced UUID lookup code with newer and slightly improved implementation * added new 1.14 wooden sign variations to safe open blocks list * fix for error if Fill was canceled or finished without actually loading any chunks * added notice to end of Trim recommending server restart --- .../com/wimbli/WorldBorder/BorderData.java | 18 +- .../wimbli/WorldBorder/UUID/NameFetcher.java | 55 ---- .../wimbli/WorldBorder/UUID/UUIDFetcher.java | 239 ++++++++++-------- .../WorldBorder/UUID/UUIDTypeAdapter.java | 32 +++ .../com/wimbli/WorldBorder/WBListener.java | 24 +- .../com/wimbli/WorldBorder/WorldFillTask.java | 20 +- .../com/wimbli/WorldBorder/WorldTrimTask.java | 4 + .../com/wimbli/WorldBorder/cmd/CmdBypass.java | 2 +- .../wimbli/WorldBorder/cmd/CmdBypasslist.java | 5 +- 9 files changed, 218 insertions(+), 181 deletions(-) delete mode 100644 src/main/java/com/wimbli/WorldBorder/UUID/NameFetcher.java create mode 100644 src/main/java/com/wimbli/WorldBorder/UUID/UUIDTypeAdapter.java diff --git a/src/main/java/com/wimbli/WorldBorder/BorderData.java b/src/main/java/com/wimbli/WorldBorder/BorderData.java index 5fb88f3..8217363 100644 --- a/src/main/java/com/wimbli/WorldBorder/BorderData.java +++ b/src/main/java/com/wimbli/WorldBorder/BorderData.java @@ -327,8 +327,6 @@ public class BorderData safeOpenBlocks.add(Material.WALL_TORCH); safeOpenBlocks.add(Material.REDSTONE_WIRE); safeOpenBlocks.add(Material.WHEAT); - safeOpenBlocks.add(Material.SIGN); - safeOpenBlocks.add(Material.WALL_SIGN); safeOpenBlocks.add(Material.LADDER); safeOpenBlocks.add(Material.LEVER); safeOpenBlocks.add(Material.LIGHT_WEIGHTED_PRESSURE_PLATE); @@ -376,6 +374,22 @@ public class BorderData safeOpenBlocks.add(Material.TALL_GRASS); safeOpenBlocks.add(Material.LARGE_FERN); safeOpenBlocks.add(Material.BEETROOTS); + try + { // signs in 1.14 can be different wood types + safeOpenBlocks.add(Material.ACACIA_SIGN); + safeOpenBlocks.add(Material.ACACIA_WALL_SIGN); + safeOpenBlocks.add(Material.BIRCH_SIGN); + safeOpenBlocks.add(Material.BIRCH_WALL_SIGN); + safeOpenBlocks.add(Material.DARK_OAK_SIGN); + safeOpenBlocks.add(Material.DARK_OAK_WALL_SIGN); + safeOpenBlocks.add(Material.JUNGLE_SIGN); + safeOpenBlocks.add(Material.JUNGLE_WALL_SIGN); + safeOpenBlocks.add(Material.OAK_SIGN); + safeOpenBlocks.add(Material.OAK_WALL_SIGN); + safeOpenBlocks.add(Material.SPRUCE_SIGN); + safeOpenBlocks.add(Material.SPRUCE_WALL_SIGN); + } + catch (NoSuchFieldError ex) {} } //these material IDs are ones we don't want to drop the player onto, like cactus or lava or fire or activated Ender portal diff --git a/src/main/java/com/wimbli/WorldBorder/UUID/NameFetcher.java b/src/main/java/com/wimbli/WorldBorder/UUID/NameFetcher.java deleted file mode 100644 index deb5517..0000000 --- a/src/main/java/com/wimbli/WorldBorder/UUID/NameFetcher.java +++ /dev/null @@ -1,55 +0,0 @@ -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; - } -} diff --git a/src/main/java/com/wimbli/WorldBorder/UUID/UUIDFetcher.java b/src/main/java/com/wimbli/WorldBorder/UUID/UUIDFetcher.java index e77e1a5..7879c30 100644 --- a/src/main/java/com/wimbli/WorldBorder/UUID/UUIDFetcher.java +++ b/src/main/java/com/wimbli/WorldBorder/UUID/UUIDFetcher.java @@ -1,116 +1,149 @@ -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 + * This code mostly taken from https://gist.github.com/Jofkos/d0c469528b032d820f42 */ -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; +package com.wimbli.WorldBorder.UUID; - public UUIDFetcher(List names, boolean rateLimiting) - { - this.names = ImmutableList.copyOf(names); - this.rateLimiting = rateLimiting; - } +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.function.Consumer; - public UUIDFetcher(List names) - { - this(names, true); - } +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; - 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(); - } +public class UUIDFetcher { + + /** + * Date when name changes were introduced + * @see UUIDFetcher#getUUIDAt(String, long) + */ + public static final long FEBRUARY_2015 = 1422748800000L; + + + private static Gson gson = new GsonBuilder().registerTypeAdapter(UUID.class, new UUIDTypeAdapter()).create(); + + private static final String UUID_URL = "https://api.mojang.com/users/profiles/minecraft/%s?at=%d"; + private static final String NAME_URL = "https://api.mojang.com/user/profiles/%s/names"; - 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 Map uuidCache = new HashMap(); + private static Map nameCache = new HashMap(); - 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)); - } + private static ExecutorService pool = Executors.newCachedThreadPool(); + + private String name; + private UUID id; + + /** + * Fetches the uuid asynchronously and passes it to the consumer + * + * @param name The name + * @param action Do what you want to do with the uuid her + */ + public static void getUUID(String name, Consumer action) { + pool.execute(() -> action.accept(getUUID(name))); + } + + /** + * Fetches the uuid synchronously and returns it + * + * @param name The name + * @return The uuid + */ + public static UUID getUUID(String name) { + return getUUIDAt(name, System.currentTimeMillis()); + } + + /** + * Fetches the uuid synchronously for a specified name and time and passes the result to the consumer + * + * @param name The name + * @param timestamp Time when the player had this name in milliseconds + * @param action Do what you want to do with the uuid her + */ + public static void getUUIDAt(String name, long timestamp, Consumer action) { + pool.execute(() -> action.accept(getUUIDAt(name, timestamp))); + } + + /** + * Fetches the uuid synchronously for a specified name and time + * + * @param name The name + * @param timestamp Time when the player had this name in milliseconds + * @see UUIDFetcher#FEBRUARY_2015 + */ + public static UUID getUUIDAt(String name, long timestamp) { + name = name.toLowerCase(); + if (uuidCache.containsKey(name)) { + return uuidCache.get(name); + } + try { + HttpURLConnection connection = (HttpURLConnection) new URL(String.format(UUID_URL, name, timestamp/1000)).openConnection(); + connection.setReadTimeout(5000); + UUIDFetcher data = gson.fromJson(new BufferedReader(new InputStreamReader(connection.getInputStream())), UUIDFetcher.class); + + uuidCache.put(name, data.id); + nameCache.put(data.id, data.name); - public static byte[] toBytes(UUID uuid) - { - ByteBuffer byteBuffer = ByteBuffer.wrap(new byte[16]); - byteBuffer.putLong(uuid.getMostSignificantBits()); - byteBuffer.putLong(uuid.getLeastSignificantBits()); - return byteBuffer.array(); - } + return data.id; + } catch (Exception e) { + e.printStackTrace(); + } + + return null; + } + + /** + * Fetches the name asynchronously and passes it to the consumer + * + * @param uuid The uuid + * @param action Do what you want to do with the name her + */ + public static void getName(UUID uuid, Consumer action) { + pool.execute(() -> action.accept(getName(uuid))); + } - 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); - } + /** + * Fetches the name synchronously and returns it + * + * @param uuid The uuid + * @return The name + */ + public static String getName(UUID uuid) { + if (nameCache.containsKey(uuid)) { + return nameCache.get(uuid); + } + try { + HttpURLConnection connection = (HttpURLConnection) new URL(String.format(NAME_URL, UUIDTypeAdapter.fromUUID(uuid))).openConnection(); + connection.setReadTimeout(5000); + UUIDFetcher[] nameHistory = gson.fromJson(new BufferedReader(new InputStreamReader(connection.getInputStream())), UUIDFetcher[].class); + UUIDFetcher currentNameData = nameHistory[nameHistory.length - 1]; - public static UUID getUUIDOf(String name) throws Exception - { - return new UUIDFetcher(Arrays.asList(name)).call().get(name.toLowerCase()); - } + uuidCache.put(currentNameData.name.toLowerCase(), uuid); + nameCache.put(uuid, currentNameData.name); + + return currentNameData.name; + } catch (Exception e) { + e.printStackTrace(); + } + + return null; + } + + public static Map getNameList(ArrayList uuids) { + Map uuidStringMap = new HashMap<>(); + for (UUID uuid: uuids) + { + uuidStringMap.put(uuid, getName(uuid)); + } + return uuidStringMap; + } } diff --git a/src/main/java/com/wimbli/WorldBorder/UUID/UUIDTypeAdapter.java b/src/main/java/com/wimbli/WorldBorder/UUID/UUIDTypeAdapter.java new file mode 100644 index 0000000..eca0114 --- /dev/null +++ b/src/main/java/com/wimbli/WorldBorder/UUID/UUIDTypeAdapter.java @@ -0,0 +1,32 @@ +/* + * This code from: https://github.com/eitetu/minecraft-server/blob/master/src/main/java/com/eitetu/minecraft/server/util/UUIDTypeAdapter.java + */ + +package com.wimbli.WorldBorder.UUID; + +import java.io.IOException; +import java.util.UUID; + +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; + + +public class UUIDTypeAdapter extends TypeAdapter { + public void write(JsonWriter out, UUID value) throws IOException { + out.value(fromUUID(value)); + } + + public UUID read(JsonReader in) throws IOException { + return fromString(in.nextString()); + } + + public static String fromUUID(UUID value) { + return value.toString().replace("-", ""); + } + + public static UUID fromString(String input) { + return UUID.fromString(input.replaceFirst( + "(\\w{8})(\\w{4})(\\w{4})(\\w{4})(\\w{12})", "$1-$2-$3-$4-$5")); + } +} \ No newline at end of file diff --git a/src/main/java/com/wimbli/WorldBorder/WBListener.java b/src/main/java/com/wimbli/WorldBorder/WBListener.java index e232fe4..949979e 100644 --- a/src/main/java/com/wimbli/WorldBorder/WBListener.java +++ b/src/main/java/com/wimbli/WorldBorder/WBListener.java @@ -7,8 +7,8 @@ import org.bukkit.event.Listener; import org.bukkit.event.player.PlayerTeleportEvent; import org.bukkit.event.player.PlayerPortalEvent; import org.bukkit.event.world.ChunkLoadEvent; -import org.bukkit.Location; import org.bukkit.event.world.ChunkUnloadEvent; +import org.bukkit.Location; public class WBListener implements Listener @@ -73,21 +73,21 @@ public class WBListener implements Listener /* * Check if there is a fill task running, and if yes, if it's for the - * world that the unload event refers to and if the chunk should be - * kept in memory because generation still needs it. + * world that the unload event refers to, set "force loaded" flag off + * and track if chunk was somehow on unload prevention list */ @EventHandler public void onChunkUnload(ChunkUnloadEvent e) { - if (Config.fillTask!=null) - { - Chunk chunk=e.getChunk(); - if (e.getWorld() == Config.fillTask.getWorld() - && Config.fillTask.chunkOnUnloadPreventionList(chunk.getX(), chunk.getZ())) - { - e.setCancelled(true); - } - } + if (Config.fillTask == null) + return; + + Chunk chunk = e.getChunk(); + if (e.getWorld() != Config.fillTask.getWorld()) + return; + + // just to be on the safe side, in case it's still set at this point somehow + chunk.setForceLoaded(false); } } diff --git a/src/main/java/com/wimbli/WorldBorder/WorldFillTask.java b/src/main/java/com/wimbli/WorldBorder/WorldFillTask.java index 5e73ebf..208a8d3 100644 --- a/src/main/java/com/wimbli/WorldBorder/WorldFillTask.java +++ b/src/main/java/com/wimbli/WorldBorder/WorldFillTask.java @@ -247,7 +247,10 @@ public class WorldFillTask implements Runnable for (CoordXZ unload: chunksToUnload) { if (!chunkOnUnloadPreventionList(unload.x, unload.z)) + { + world.setChunkForceLoaded(unload.x, unload.z, false); world.unloadChunkRequest(unload.x, unload.z); + } } // Put some damper on chunksPerRun. We don't want the queue to be too @@ -312,6 +315,7 @@ public class WorldFillTask implements Runnable } } + world.setChunkForceLoaded(x, z, true); // toggle "force loaded" flag on for chunk to prevent it from being unloaded while we need it pendingChunks.put(PaperLib.getChunkAtAsync(world, x, z, true), new CoordXZ(x, z)); // There need to be enough nearby chunks loaded to make the server populate a chunk with trees, snow, etc. @@ -319,10 +323,12 @@ public class WorldFillTask implements Runnable int popX = !isZLeg ? x : (x + (isNeg ? -1 : 1)); int popZ = isZLeg ? z : (z + (!isNeg ? -1 : 1)); + world.setChunkForceLoaded(popX, popZ, true); pendingChunks.put(PaperLib.getChunkAtAsync(world, popX, popZ, false), new CoordXZ(popX, popZ)); preventUnload.add(new UnloadDependency(popX, popZ, x, z)); // make sure the previous chunk in our spiral is loaded as well (might have already existed and been skipped over) + world.setChunkForceLoaded(lastChunk.x, lastChunk.z, true); pendingChunks.put(PaperLib.getChunkAtAsync(world, lastChunk.x, lastChunk.z, false), new CoordXZ(lastChunk.x, lastChunk.z)); // <-- new CoordXZ as lastChunk isn't immutable preventUnload.add(new UnloadDependency(lastChunk.x, lastChunk.z, x, z)); @@ -437,13 +443,17 @@ public class WorldFillTask implements Runnable server = null; // go ahead and unload any chunks we still have loaded - // Set preventUnload to emptry first so the ChunkUnloadEvent Listener + // Set preventUnload to empty first so the ChunkUnloadEvent Listener // doesn't get in our way - Set tempPreventUnload = preventUnload; - preventUnload = null; - for (UnloadDependency entry: tempPreventUnload) + if (preventUnload != null) { - world.unloadChunkRequest(entry.neededX, entry.neededZ); + Set tempPreventUnload = preventUnload; + preventUnload = null; + for (UnloadDependency entry: tempPreventUnload) + { + world.setChunkForceLoaded(entry.neededX, entry.neededZ, false); + world.unloadChunkRequest(entry.neededX, entry.neededZ); + } } } diff --git a/src/main/java/com/wimbli/WorldBorder/WorldTrimTask.java b/src/main/java/com/wimbli/WorldBorder/WorldTrimTask.java index c593bf5..60fa450 100644 --- a/src/main/java/com/wimbli/WorldBorder/WorldTrimTask.java +++ b/src/main/java/com/wimbli/WorldBorder/WorldTrimTask.java @@ -363,6 +363,10 @@ public class WorldTrimTask implements Runnable if (taskID != -1) server.getScheduler().cancelTask(taskID); server = null; + + sendMessage("NOTICE: it is recommended that you restart your server after a Trim, to be on the safe side."); + if (DynMapFeatures.renderEnabled()) + sendMessage("This especially true with DynMap. You should also run a fullrender in DynMap for the trimmed world after restarting, so trimmed chunks are updated on the map."); } // is this task still valid/workable? diff --git a/src/main/java/com/wimbli/WorldBorder/cmd/CmdBypass.java b/src/main/java/com/wimbli/WorldBorder/cmd/CmdBypass.java index fec9b35..605c3b8 100644 --- a/src/main/java/com/wimbli/WorldBorder/cmd/CmdBypass.java +++ b/src/main/java/com/wimbli/WorldBorder/cmd/CmdBypass.java @@ -65,7 +65,7 @@ public class CmdBypass extends WBCmd // only do UUID lookup using Mojang server if specified player isn't online try { - uPlayer = UUIDFetcher.getUUIDOf(sPlayer); + uPlayer = UUIDFetcher.getUUID(sPlayer); } catch(Exception ex) { diff --git a/src/main/java/com/wimbli/WorldBorder/cmd/CmdBypasslist.java b/src/main/java/com/wimbli/WorldBorder/cmd/CmdBypasslist.java index 3181160..2e79ae9 100644 --- a/src/main/java/com/wimbli/WorldBorder/cmd/CmdBypasslist.java +++ b/src/main/java/com/wimbli/WorldBorder/cmd/CmdBypasslist.java @@ -10,7 +10,7 @@ import org.bukkit.command.*; import org.bukkit.entity.Player; import com.wimbli.WorldBorder.*; -import com.wimbli.WorldBorder.UUID.NameFetcher; +import com.wimbli.WorldBorder.UUID.UUIDFetcher; public class CmdBypasslist extends WBCmd @@ -42,8 +42,7 @@ public class CmdBypasslist extends WBCmd { try { - NameFetcher fetcher = new NameFetcher(uuids); - Map names = fetcher.call(); + Map names = UUIDFetcher.getNameList(uuids); String nameString = names.values().toString(); sender.sendMessage("Players with border bypass enabled: " + nameString.substring(1, nameString.length() - 1));