diff --git a/pom.xml b/pom.xml index a2a1023..aa9af14 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 com.wimbli.WorldBorder WorldBorder - 1.8.5 + 1.8.8 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.5.1 - 1.6 - 1.6 + 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 ae50e40..12f5f9c 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; @@ -17,7 +18,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 { @@ -36,8 +37,97 @@ 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 amorphous + * 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. + * + * 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 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 Location prior, final long maxDelay, final long recheckDelay) { + + // 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 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); + if (Config.Debug()) + Config.log("Minecraft caught up with " + playerName + ". Ending watch."); + return; + } + + if (Config.Debug()) { + Config.log("Based on teleport " + playerName + + " has moved, but Minecraft still has them at old location " + + prior.toString() + ". 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("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 public static Location checkPlayer(Player player, Location targetLoc, boolean returnLocationOnly, boolean notify) @@ -56,11 +146,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 457a1e2..1b912ae 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; @@ -544,6 +546,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) @@ -605,6 +622,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(); @@ -713,6 +732,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()) @@ -751,4 +772,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 dba5cfe..3dd0df3 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,8 +18,11 @@ public class WBListener implements Listener if (Config.KnockBack() == 0.0) return; - if (Config.Debug()) - Config.log("Teleport cause: " + event.getCause().toString()); + if (event instanceof PlayerPortalEvent) { // Avoid overlapping management. + if (Config.Debug()) + 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 +40,20 @@ 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(), event.getFrom().clone(), + Config.getMaxExemptionTicks(), Config.getPortalRecheckTicks()); } @EventHandler(priority = EventPriority.MONITOR) 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