From 80f772a95c5fabb23918d9b39e9400f80bc46c2e Mon Sep 17 00:00:00 2001 From: Daniel Boston Date: Wed, 23 Mar 2016 05:19:05 -0400 Subject: [PATCH 1/4] Updating pom to latest spigot/bukkit api and updating plugin.yml to track with version and name info from pom. --- pom.xml | 20 +++++++++++++------- src/main/resources/plugin.yml | 4 ++-- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/pom.xml b/pom.xml index a2a1023..464f384 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 com.wimbli.WorldBorder WorldBorder - 1.8.5 + 1.8.7 WorldBorder https://github.com/Brettflan/WorldBorder @@ -31,13 +31,13 @@ org.spigotmc spigot-api - 1.8.7-R0.1-SNAPSHOT + 1.9-R0.1-SNAPSHOT org.bukkit bukkit - 1.8.7-R0.1-SNAPSHOT + 1.9-R0.1-SNAPSHOT @@ -49,15 +49,21 @@ clean install - ${project.artifactId} + ${project.artifactId}-${project.version} + + + src/main/resources + true + + org.apache.maven.plugins maven-compiler-plugin - 2.0.2 + 3.0 - 1.6 - 1.6 + 1.7 + 1.7 diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index d2aa9b7..413d9ec 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -1,7 +1,7 @@ -name: WorldBorder +name: ${project.name} author: Brettflan description: Efficient, feature-rich plugin for limiting the size of your worlds. -version: 1.8.5 +version: ${project.version} main: com.wimbli.WorldBorder.WorldBorder softdepend: - dynmap From 4b6ac2a3d8444303780843d316b2ea02a72b09e4 Mon Sep 17 00:00:00 2001 From: Daniel Boston Date: Tue, 22 Mar 2016 02:01:01 -0400 Subject: [PATCH 2/4] Fixing nether teleportation player data eventual consistency problem via timed exemption. --- .../wimbli/WorldBorder/BorderCheckTask.java | 26 ++++++++++++++++++- .../com/wimbli/WorldBorder/WBListener.java | 15 +++++++++-- 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/wimbli/WorldBorder/BorderCheckTask.java b/src/main/java/com/wimbli/WorldBorder/BorderCheckTask.java index ae50e40..552bd16 100644 --- a/src/main/java/com/wimbli/WorldBorder/BorderCheckTask.java +++ b/src/main/java/com/wimbli/WorldBorder/BorderCheckTask.java @@ -17,7 +17,7 @@ import org.bukkit.event.player.PlayerTeleportEvent.TeleportCause; import org.bukkit.Location; import org.bukkit.util.Vector; import org.bukkit.World; - +import org.bukkit.scheduler.BukkitRunnable; public class BorderCheckTask implements Runnable { @@ -39,6 +39,30 @@ public class BorderCheckTask implements Runnable // track players who are being handled (moved back inside the border) already; needed since Bukkit is sometimes sending teleport events with the old (now incorrect) location still indicated, which can lead to a loop when we then teleport them thinking they're outside the border, triggering event again, etc. private static Set handlingPlayers = Collections.synchronizedSet(new LinkedHashSet()); + /** + * In 1.9, there is a significant delay between teleportation event and when the player's location is actually updated. + * However, the player world is updated immediately. This disconnection causes the regular checkPlayer to + * incorrectly test the player's prior-world location against the new-world location during that inbetween period. + * + * This function allows a temporary delay against the check to let Minecraft "catch up" the player's _real_ location. + */ + public static void timedPlayerExemption(final Player player, long delay) { + handlingPlayers.add(player.getName().toLowerCase()); + + new BukkitRunnable() { + private final String playerName = player.getName().toLowerCase(); + @Override + public void run() { + handlingPlayers.remove(playerName); + if (Config.Debug()) + Config.log("Exemption for " + playerName + " expired"); + } + }.runTaskLater(WorldBorder.plugin, delay); + + if (Config.Debug()) + Config.log("Exempting " + player.getName().toLowerCase() + " for " + delay + " ticks."); + } + // set targetLoc only if not current player location; set returnLocationOnly to true to have new Location returned if they need to be moved to one, instead of directly handling it public static Location checkPlayer(Player player, Location targetLoc, boolean returnLocationOnly, boolean notify) { diff --git a/src/main/java/com/wimbli/WorldBorder/WBListener.java b/src/main/java/com/wimbli/WorldBorder/WBListener.java index dba5cfe..8bfa58c 100644 --- a/src/main/java/com/wimbli/WorldBorder/WBListener.java +++ b/src/main/java/com/wimbli/WorldBorder/WBListener.java @@ -20,7 +20,12 @@ public class WBListener implements Listener return; if (Config.Debug()) - Config.log("Teleport cause: " + event.getCause().toString()); + Config.log("General Teleport cause: " + event.getCause().toString()); + + if (PlayerTeleportEvent.TeleportCause.NETHER_PORTAL == event.getCause()) { + Config.log("Skipping teleport management event - covered by onPlayerPortal"); + return; + } Location newLoc = BorderCheckTask.checkPlayer(event.getPlayer(), event.getTo(), true, true); if (newLoc != null) @@ -38,13 +43,19 @@ public class WBListener implements Listener @EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true) public void onPlayerPortal(PlayerPortalEvent event) { + if (Config.Debug()) + Config.log("Player Portal Teleport cause: " + event.getCause().toString()); + // if knockback is set to 0, or portal redirection is disabled, simply return if (Config.KnockBack() == 0.0 || !Config.portalRedirection()) return; Location newLoc = BorderCheckTask.checkPlayer(event.getPlayer(), event.getTo(), true, false); - if (newLoc != null) + if (newLoc != null) { event.setTo(newLoc); + } + + BorderCheckTask.timedPlayerExemption(event.getPlayer(), 100l); } @EventHandler(priority = EventPriority.MONITOR) From 116718e1a29809a6547a4349400a3b68b75de4e0 Mon Sep 17 00:00:00 2001 From: Daniel Boston Date: Wed, 15 Jun 2016 22:27:48 -0400 Subject: [PATCH 3/4] Maturing the border exemption; instead of a blanket exemption this starts a check task to see if the player's world has caught up with their portaled location. --- pom.xml | 4 +- .../wimbli/WorldBorder/BorderCheckTask.java | 105 ++++++++++++++---- .../java/com/wimbli/WorldBorder/Config.java | 22 ++++ .../com/wimbli/WorldBorder/WBListener.java | 12 +- 4 files changed, 114 insertions(+), 29 deletions(-) diff --git a/pom.xml b/pom.xml index 464f384..aa9af14 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 com.wimbli.WorldBorder WorldBorder - 1.8.7 + 1.8.8 WorldBorder https://github.com/Brettflan/WorldBorder @@ -60,7 +60,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.0 + 3.5.1 1.7 1.7 diff --git a/src/main/java/com/wimbli/WorldBorder/BorderCheckTask.java b/src/main/java/com/wimbli/WorldBorder/BorderCheckTask.java index 552bd16..cc07b70 100644 --- a/src/main/java/com/wimbli/WorldBorder/BorderCheckTask.java +++ b/src/main/java/com/wimbli/WorldBorder/BorderCheckTask.java @@ -2,8 +2,9 @@ package com.wimbli.WorldBorder; import java.util.Collection; import java.util.Collections; -import java.util.LinkedHashSet; -import java.util.Set; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.UUID; import com.google.common.collect.ImmutableList; @@ -36,31 +37,95 @@ public class BorderCheckTask implements Runnable } } - // track players who are being handled (moved back inside the border) already; needed since Bukkit is sometimes sending teleport events with the old (now incorrect) location still indicated, which can lead to a loop when we then teleport them thinking they're outside the border, triggering event again, etc. - private static Set handlingPlayers = Collections.synchronizedSet(new LinkedHashSet()); + /** track players who are being handled (moved back inside the border) already; needed + * since Bukkit is sometimes sending teleport events with the old (now incorrect) location + * still indicated, which can lead to a loop when we then teleport them thinking they're + * outside the border, triggering event again, etc. + */ + private static Map handlingPlayers = Collections.synchronizedMap(new LinkedHashMap()); /** * In 1.9, there is a significant delay between teleportation event and when the player's location is actually updated. * However, the player world is updated immediately. This disconnection causes the regular checkPlayer to - * incorrectly test the player's prior-world location against the new-world location during that inbetween period. + * incorrectly test the player's prior-world location against the new-world location during that amorphous + * in-between period. * - * This function allows a temporary delay against the check to let Minecraft "catch up" the player's _real_ location. + * This function allows a configurable recheck to let Minecraft "catch up" the player's real location. + * + * In the meantime, the player is exempted from border crossing checks (and from spurious additional teleport event + * checks). + * + * Note that additional portal teleports (e.g. if the player immediately portals back) will reset + * this check gracefully. + * + * @param player The player who is being exempted. + * @param world The world the player is supposedly now in. + * @param maxDelay The maximum ticks to spend exempting this player + * @param recheckDelay The ticks to wait inbetween rechecks. */ - public static void timedPlayerExemption(final Player player, long delay) { - handlingPlayers.add(player.getName().toLowerCase()); + public static void timedPlayerExemption(final Player player, final String world, final long maxDelay, final long recheckDelay) { - new BukkitRunnable() { - private final String playerName = player.getName().toLowerCase(); - @Override - public void run() { - handlingPlayers.remove(playerName); - if (Config.Debug()) - Config.log("Exemption for " + playerName + " expired"); - } - }.runTaskLater(WorldBorder.plugin, delay); + // Check for existing watch; cancel if one exists. + BukkitRunnable alreadyWatching = handlingPlayers.get(player.getName().toLowerCase()); + if (alreadyWatching != null) { + try { + alreadyWatching.cancel(); + } catch (IllegalStateException e){} + } + + alreadyWatching = new BukkitRunnable() { + private final String playerName = player.getName().toLowerCase(); + private final UUID playerUUID = player.getUniqueId(); + private long currentDelay = recheckDelay; + @Override + public void run() { + // Are we done checking? + if (currentDelay > maxDelay) { + this.cancel(); + handlingPlayers.remove(playerName); + if (Config.Debug()) + Config.log("Done watching " + playerName + ". They are in the hands of fate, now."); + return; + } + currentDelay += recheckDelay; + + // Is this player still online? + Player player = Bukkit.getPlayer(playerUUID); + if (player == null) { // assume offline + this.cancel(); + handlingPlayers.remove(playerName); + if (Config.Debug()) + Config.log("Looks like " + playerName + " logged off. Suspending watch."); + return; + } + + // Are we still stuck between worlds? + Location loc = player.getLocation(); + World worldObj = loc.getWorld(); + if (world.equals(worldObj.getName())) { + // No, we made it! + this.cancel(); + handlingPlayers.remove(playerName); + if (Config.Debug()) + Config.log("Minecraft caught up with " + playerName + ". Ending watch."); + return; + } + + if (Config.Debug()) { + Config.log("Based on teleport " + playerName + " is in " + world + + " but Minecraft still thinks they are in " + worldObj.getName() + + ". Checking again in " + recheckDelay); + } + } + }; + + // Store the exemption and start the recheck. + handlingPlayers.put(player.getName().toLowerCase(), alreadyWatching); + alreadyWatching.runTaskTimer(WorldBorder.plugin, recheckDelay, recheckDelay); if (Config.Debug()) - Config.log("Exempting " + player.getName().toLowerCase() + " for " + delay + " ticks."); + Config.log("Rechecking " + player.getName() + "'s world every " + + recheckDelay + " ticks for " + maxDelay + " ticks."); } // set targetLoc only if not current player location; set returnLocationOnly to true to have new Location returned if they need to be moved to one, instead of directly handling it @@ -80,11 +145,11 @@ 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.getUniqueId()) || handlingPlayers.contains(player.getName().toLowerCase())) + if (Config.isPlayerBypassing(player.getUniqueId()) || handlingPlayers.containsKey(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 - handlingPlayers.add(player.getName().toLowerCase()); + handlingPlayers.put(player.getName().toLowerCase(), null); Location newLoc = newLocation(player, loc, border, notify); boolean handlingVehicle = false; diff --git a/src/main/java/com/wimbli/WorldBorder/Config.java b/src/main/java/com/wimbli/WorldBorder/Config.java index 0ea4f09..bcce465 100644 --- a/src/main/java/com/wimbli/WorldBorder/Config.java +++ b/src/main/java/com/wimbli/WorldBorder/Config.java @@ -56,6 +56,8 @@ public class Config private static int fillMemoryTolerance = 500; private static boolean preventBlockPlace = false; private static boolean preventMobSpawn = false; + private static long maxExemptionTicks = 21l; + private static long portalRecheckTicks = 4l; // These together give 5 recheck chances over the course of a second. // for monitoring plugin efficiency // public static long timeUsed = 0; @@ -539,6 +541,21 @@ public class Config return false; } + + + public static long getMaxExemptionTicks() { + return maxExemptionTicks; + } + public static void setMaxExemptionTicks(long maxExemptionTicks) { + Config.maxExemptionTicks = maxExemptionTicks; + } + + public static long getPortalRecheckTicks() { + return portalRecheckTicks; + } + public static void setPortalRecheckTicks(long portalRecheckTicks) { + Config.portalRecheckTicks = portalRecheckTicks; + } public static String replaceAmpColors (String message) @@ -600,6 +617,8 @@ public class Config fillMemoryTolerance = cfg.getInt("fill-memory-tolerance", 500); preventBlockPlace = cfg.getBoolean("prevent-block-place"); preventMobSpawn = cfg.getBoolean("prevent-mob-spawn"); + maxExemptionTicks = cfg.getLong("max-exemption-ticks", 21l); + portalRecheckTicks = cfg.getLong("portal-recheck-ticks", 4l); StartBorderTimer(); @@ -708,6 +727,8 @@ public class Config cfg.set("fill-memory-tolerance", fillMemoryTolerance); cfg.set("prevent-block-place", preventBlockPlace); cfg.set("prevent-mob-spawn", preventMobSpawn); + cfg.set("max-exemption-ticks", maxExemptionTicks); + cfg.set("portal-recheck-ticks", portalRecheckTicks); cfg.set("worlds", null); for(Entry stringBorderDataEntry : borders.entrySet()) @@ -746,4 +767,5 @@ public class Config if (logIt) logConfig("Configuration saved."); } + } diff --git a/src/main/java/com/wimbli/WorldBorder/WBListener.java b/src/main/java/com/wimbli/WorldBorder/WBListener.java index 8bfa58c..0ad71f1 100644 --- a/src/main/java/com/wimbli/WorldBorder/WBListener.java +++ b/src/main/java/com/wimbli/WorldBorder/WBListener.java @@ -9,7 +9,6 @@ import org.bukkit.event.player.PlayerPortalEvent; import org.bukkit.event.world.ChunkLoadEvent; import org.bukkit.Location; - public class WBListener implements Listener { @EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true) @@ -19,11 +18,9 @@ public class WBListener implements Listener if (Config.KnockBack() == 0.0) return; - if (Config.Debug()) - Config.log("General Teleport cause: " + event.getCause().toString()); - - if (PlayerTeleportEvent.TeleportCause.NETHER_PORTAL == event.getCause()) { - Config.log("Skipping teleport management event - covered by onPlayerPortal"); + if (event instanceof PlayerPortalEvent) { // Avoid overlapping management. + if (Config.Debug()) + Config.log("Skipping teleport management event - covered by onPlayerPortal"); return; } @@ -55,7 +52,8 @@ public class WBListener implements Listener event.setTo(newLoc); } - BorderCheckTask.timedPlayerExemption(event.getPlayer(), 100l); + BorderCheckTask.timedPlayerExemption(event.getPlayer(), event.getTo().getWorld().getName(), + Config.getMaxExemptionTicks(), Config.getPortalRecheckTicks()); } @EventHandler(priority = EventPriority.MONITOR) From bc36bcbee344b112220006733810ed8dd670004f Mon Sep 17 00:00:00 2001 From: Daniel Boston Date: Thu, 16 Jun 2016 22:25:08 -0400 Subject: [PATCH 4/4] Correcting earlier misconception. This new check routine keeps up the exemption until the players location is updated to something other then the FROM location of the portal event. The minecraft bug is that the from location dominates even in the new world, and only after a delay (which can last for, in some cases, a second or more) is the players location updated to the portal event TO location. This check adapts for that, maintaining an exemption until the players location is updated to something other then the portal event FROM location, or the check period ends. --- .../wimbli/WorldBorder/BorderCheckTask.java | 19 ++++++++++--------- .../com/wimbli/WorldBorder/WBListener.java | 2 +- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/wimbli/WorldBorder/BorderCheckTask.java b/src/main/java/com/wimbli/WorldBorder/BorderCheckTask.java index cc07b70..12f5f9c 100644 --- a/src/main/java/com/wimbli/WorldBorder/BorderCheckTask.java +++ b/src/main/java/com/wimbli/WorldBorder/BorderCheckTask.java @@ -48,7 +48,8 @@ public class BorderCheckTask implements Runnable * In 1.9, there is a significant delay between teleportation event and when the player's location is actually updated. * However, the player world is updated immediately. This disconnection causes the regular checkPlayer to * incorrectly test the player's prior-world location against the new-world location during that amorphous - * in-between period. + * in-between period. Basically, this checks for the location to actually update, e.g. to be different from the + * "from" location of the portal event. * * This function allows a configurable recheck to let Minecraft "catch up" the player's real location. * @@ -59,11 +60,11 @@ public class BorderCheckTask implements Runnable * this check gracefully. * * @param player The player who is being exempted. - * @param world The world the player is supposedly now in. + * @param prior the location the player has come from * @param maxDelay The maximum ticks to spend exempting this player * @param recheckDelay The ticks to wait inbetween rechecks. */ - public static void timedPlayerExemption(final Player player, final String world, final long maxDelay, final long recheckDelay) { + public static void timedPlayerExemption(final Player player, final Location prior, final long maxDelay, final long recheckDelay) { // Check for existing watch; cancel if one exists. BukkitRunnable alreadyWatching = handlingPlayers.get(player.getName().toLowerCase()); @@ -100,9 +101,9 @@ public class BorderCheckTask implements Runnable } // Are we still stuck between worlds? - Location loc = player.getLocation(); - World worldObj = loc.getWorld(); - if (world.equals(worldObj.getName())) { + Location current = player.getLocation(); + if (current.getBlockX() != prior.getBlockX() && current.getBlockY() != prior.getBlockY() && + current.getBlockZ() != prior.getBlockZ()) { // No, we made it! this.cancel(); handlingPlayers.remove(playerName); @@ -112,9 +113,9 @@ public class BorderCheckTask implements Runnable } if (Config.Debug()) { - Config.log("Based on teleport " + playerName + " is in " + world + - " but Minecraft still thinks they are in " + worldObj.getName() + - ". Checking again in " + recheckDelay); + Config.log("Based on teleport " + playerName + + " has moved, but Minecraft still has them at old location " + + prior.toString() + ". Checking again in " + recheckDelay); } } }; diff --git a/src/main/java/com/wimbli/WorldBorder/WBListener.java b/src/main/java/com/wimbli/WorldBorder/WBListener.java index 0ad71f1..3dd0df3 100644 --- a/src/main/java/com/wimbli/WorldBorder/WBListener.java +++ b/src/main/java/com/wimbli/WorldBorder/WBListener.java @@ -52,7 +52,7 @@ public class WBListener implements Listener event.setTo(newLoc); } - BorderCheckTask.timedPlayerExemption(event.getPlayer(), event.getTo().getWorld().getName(), + BorderCheckTask.timedPlayerExemption(event.getPlayer(), event.getFrom().clone(), Config.getMaxExemptionTicks(), Config.getPortalRecheckTicks()); }