From 924a419b47a90fb1e3c9e0ae94e0f1ea24da364e Mon Sep 17 00:00:00 2001 From: Andreas Troelsen Date: Sun, 5 Aug 2018 21:17:05 +0200 Subject: [PATCH] Add join-interrupt-timer per-arena setting. This commit adds a new per-arena setting, join-interrupt-timer, which, when set to a positive number, will introduce a delay to the join and spec commands. During this delay, if the player takes damage or moves more than one block's distance, the command will be interrupted. Closes #482 --- changelog.md | 1 + .../garbagemule/MobArena/ArenaMasterImpl.java | 9 + .../java/com/garbagemule/MobArena/Msg.java | 3 + .../MobArena/commands/user/JoinCommand.java | 52 ++++-- .../MobArena/commands/user/SpecCommand.java | 51 ++++-- .../MobArena/framework/ArenaMaster.java | 3 + .../MobArena/util/JoinInterruptTimer.java | 154 ++++++++++++++++++ src/main/resources/res/settings.yml | 1 + 8 files changed, 240 insertions(+), 34 deletions(-) create mode 100644 src/main/java/com/garbagemule/MobArena/util/JoinInterruptTimer.java diff --git a/changelog.md b/changelog.md index 3c582b4..a72a955 100644 --- a/changelog.md +++ b/changelog.md @@ -12,6 +12,7 @@ These changes will (most likely) be included in the next version. ## [Unreleased] - It is now possible to add a fixed delay (in seconds) between waves with the new per-arena setting `next-wave-delay`. +- The new per-arena setting `join-interrupt-timer` makes it possible to add a "delay" to the join and spec commands. If the player moves or takes damage during this delay, the command is interrupted. This should help prevent exploits on PvP servers. - Right-clicking is now allowed in the lobby. This makes it possible to activate blocks like buttons and levers. - Snow and ice no longer melts in arenas. diff --git a/src/main/java/com/garbagemule/MobArena/ArenaMasterImpl.java b/src/main/java/com/garbagemule/MobArena/ArenaMasterImpl.java index 0ddf65b..677d41a 100644 --- a/src/main/java/com/garbagemule/MobArena/ArenaMasterImpl.java +++ b/src/main/java/com/garbagemule/MobArena/ArenaMasterImpl.java @@ -6,6 +6,7 @@ import static com.garbagemule.MobArena.util.config.ConfigUtils.parseLocation; import com.garbagemule.MobArena.framework.Arena; import com.garbagemule.MobArena.framework.ArenaMaster; import com.garbagemule.MobArena.things.Thing; +import com.garbagemule.MobArena.util.JoinInterruptTimer; import com.garbagemule.MobArena.util.config.ConfigUtils; import org.bukkit.Bukkit; import org.bukkit.Location; @@ -44,6 +45,8 @@ public class ArenaMasterImpl implements ArenaMaster private boolean enabled; + private JoinInterruptTimer joinInterruptTimer; + /** * Default constructor. */ @@ -60,6 +63,8 @@ public class ArenaMasterImpl implements ArenaMaster this.spawnsPets = new SpawnsPets(Material.BONE, Material.RAW_FISH); this.enabled = config.getBoolean("global-settings.enabled", true); + + this.joinInterruptTimer = new JoinInterruptTimer(); } /* @@ -114,6 +119,10 @@ public class ArenaMasterImpl implements ArenaMaster return allowedCommands.contains(command); } + public JoinInterruptTimer getJoinInterruptTimer() { + return joinInterruptTimer; + } + /* * ///////////////////////////////////////////////////////////////////////// * // // Arena getters // diff --git a/src/main/java/com/garbagemule/MobArena/Msg.java b/src/main/java/com/garbagemule/MobArena/Msg.java index 1588291..453886f 100644 --- a/src/main/java/com/garbagemule/MobArena/Msg.java +++ b/src/main/java/com/garbagemule/MobArena/Msg.java @@ -30,6 +30,9 @@ public enum Msg { JOIN_PLAYER_LIMIT_REACHED("The player limit of this arena has been reached."), JOIN_STORE_INV_FAIL("Failed to store inventory. Try again."), JOIN_EXISTING_INV_RESTORED("Your old inventory items have been restored."), + JOIN_AFTER_DELAY("Joining arena in &c%&r seconds..."), + JOIN_INTERRUPTED_BY_DAMAGE("Join aborted due to taking damage."), + JOIN_INTERRUPTED_BY_MOVEMENT("Join aborted due to movement."), JOIN_PLAYER_JOINED("You joined the arena. Have fun!"), LEAVE_NOT_PLAYING("You are not in the arena."), LEAVE_NOT_READY("You did not ready up in time! Next time, ready up by clicking an iron block."), diff --git a/src/main/java/com/garbagemule/MobArena/commands/user/JoinCommand.java b/src/main/java/com/garbagemule/MobArena/commands/user/JoinCommand.java index f46cb58..6dfec9b 100644 --- a/src/main/java/com/garbagemule/MobArena/commands/user/JoinCommand.java +++ b/src/main/java/com/garbagemule/MobArena/commands/user/JoinCommand.java @@ -1,5 +1,6 @@ package com.garbagemule.MobArena.commands.user; +import com.garbagemule.MobArena.MobArena; import com.garbagemule.MobArena.Msg; import com.garbagemule.MobArena.commands.Command; import com.garbagemule.MobArena.commands.CommandInfo; @@ -31,26 +32,43 @@ public class JoinCommand implements Command // Run some rough sanity checks, and grab the arena to join. Arena toArena = Commands.getArenaToJoinOrSpec(am, p, arg1); - if (toArena == null) { + if (toArena == null || !canJoin(p, toArena)) { return true; } - // Deny joining from other arenas - Arena fromArena = am.getArenaWithPlayer(p); - if (fromArena != null && (fromArena.inArena(p) || fromArena.inLobby(p))) { - fromArena.getMessenger().tell(p, Msg.JOIN_ALREADY_PLAYING); - return true; - } - - // Per-arena sanity checks - if (!toArena.canJoin(p)) { - return true; - } - - // Force leave previous arena - if (fromArena != null) fromArena.playerLeave(p); - // Join the arena! - return toArena.playerJoin(p, p.getLocation()); + int seconds = toArena.getSettings().getInt("join-interrupt-timer", 0); + if (seconds > 0) { + boolean started = am.getJoinInterruptTimer().start(p, toArena, seconds, () -> tryJoin(p, toArena)); + if (started) { + toArena.getMessenger().tell(p, Msg.JOIN_AFTER_DELAY, String.valueOf(seconds)); + } else { + toArena.getMessenger().tell(p, Msg.JOIN_ALREADY_PLAYING); + } + } else { + tryJoin(p, toArena); + } + return true; + } + + private boolean canJoin(Player player, Arena arena) { + MobArena plugin = arena.getPlugin(); + ArenaMaster am = plugin.getArenaMaster(); + if (am.getJoinInterruptTimer().isWaiting(player)) { + plugin.getGlobalMessenger().tell(player, Msg.JOIN_ALREADY_PLAYING); + return false; + } + Arena current = am.getArenaWithPlayer(player); + if (current != null) { + current.getMessenger().tell(player, Msg.JOIN_ALREADY_PLAYING); + return false; + } + return arena.canJoin(player); + } + + private void tryJoin(Player player, Arena arena) { + if (canJoin(player, arena)) { + arena.playerJoin(player, player.getLocation()); + } } } diff --git a/src/main/java/com/garbagemule/MobArena/commands/user/SpecCommand.java b/src/main/java/com/garbagemule/MobArena/commands/user/SpecCommand.java index 912d3bd..605e29c 100644 --- a/src/main/java/com/garbagemule/MobArena/commands/user/SpecCommand.java +++ b/src/main/java/com/garbagemule/MobArena/commands/user/SpecCommand.java @@ -1,5 +1,6 @@ package com.garbagemule.MobArena.commands.user; +import com.garbagemule.MobArena.MobArena; import com.garbagemule.MobArena.Msg; import com.garbagemule.MobArena.commands.Command; import com.garbagemule.MobArena.commands.CommandInfo; @@ -31,27 +32,43 @@ public class SpecCommand implements Command // Run some rough sanity checks, and grab the arena to spec. Arena toArena = Commands.getArenaToJoinOrSpec(am, p, arg1); - if (toArena == null) { + if (toArena == null || !canSpec(p, toArena)) { return true; } - // Deny spectating from other arenas - Arena fromArena = am.getArenaWithPlayer(p); - if (fromArena != null && (fromArena.inArena(p) || fromArena.inLobby(p))) { - fromArena.getMessenger().tell(p, Msg.SPEC_ALREADY_PLAYING); - return true; - } - - // Per-arena sanity checks - if (!toArena.canSpec(p)) { - return true; - } - - // Force leave previous arena - if (fromArena != null) fromArena.playerLeave(p); - // Spec the arena! - toArena.playerSpec(p, p.getLocation()); + int seconds = toArena.getSettings().getInt("join-interrupt-timer", 0); + if (seconds > 0) { + boolean started = am.getJoinInterruptTimer().start(p, toArena, seconds, () -> trySpec(p, toArena)); + if (started) { + toArena.getMessenger().tell(p, Msg.JOIN_AFTER_DELAY, String.valueOf(seconds)); + } else { + toArena.getMessenger().tell(p, Msg.SPEC_ALREADY_PLAYING); + } + } else { + trySpec(p, toArena); + } return true; } + + private boolean canSpec(Player player, Arena arena) { + MobArena plugin = arena.getPlugin(); + ArenaMaster am = plugin.getArenaMaster(); + if (am.getJoinInterruptTimer().isWaiting(player)) { + plugin.getGlobalMessenger().tell(player, Msg.SPEC_ALREADY_PLAYING); + return false; + } + Arena current = arena.getPlugin().getArenaMaster().getArenaWithPlayer(player); + if (current != null) { + current.getMessenger().tell(player, Msg.SPEC_ALREADY_PLAYING); + return false; + } + return arena.canSpec(player); + } + + private void trySpec(Player player, Arena arena) { + if (canSpec(player, arena)) { + arena.playerSpec(player, player.getLocation()); + } + } } diff --git a/src/main/java/com/garbagemule/MobArena/framework/ArenaMaster.java b/src/main/java/com/garbagemule/MobArena/framework/ArenaMaster.java index 98579e4..e9cfa7e 100644 --- a/src/main/java/com/garbagemule/MobArena/framework/ArenaMaster.java +++ b/src/main/java/com/garbagemule/MobArena/framework/ArenaMaster.java @@ -4,6 +4,7 @@ import com.garbagemule.MobArena.ArenaClass; import com.garbagemule.MobArena.Messenger; import com.garbagemule.MobArena.MobArena; import com.garbagemule.MobArena.SpawnsPets; +import com.garbagemule.MobArena.util.JoinInterruptTimer; import org.bukkit.Location; import org.bukkit.World; import org.bukkit.entity.Entity; @@ -87,6 +88,8 @@ public interface ArenaMaster Arena getArenaWithName(Collection arenas, String configName); boolean isAllowed(String command); + + JoinInterruptTimer getJoinInterruptTimer(); diff --git a/src/main/java/com/garbagemule/MobArena/util/JoinInterruptTimer.java b/src/main/java/com/garbagemule/MobArena/util/JoinInterruptTimer.java new file mode 100644 index 0000000..99e94f8 --- /dev/null +++ b/src/main/java/com/garbagemule/MobArena/util/JoinInterruptTimer.java @@ -0,0 +1,154 @@ +package com.garbagemule.MobArena.util; + +import com.garbagemule.MobArena.Msg; +import com.garbagemule.MobArena.framework.Arena; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.HandlerList; +import org.bukkit.event.Listener; +import org.bukkit.event.entity.EntityDamageEvent; +import org.bukkit.event.player.PlayerKickEvent; +import org.bukkit.event.player.PlayerMoveEvent; +import org.bukkit.event.player.PlayerQuitEvent; +import org.bukkit.scheduler.BukkitRunnable; + +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; + +public class JoinInterruptTimer { + private Set waiting; + + public JoinInterruptTimer() { + this.waiting = new HashSet<>(); + } + + public boolean isWaiting(Player player) { + return waiting.contains(player.getUniqueId()); + } + + public boolean start(Player player, Arena arena, int seconds, Runnable completed) { + if (isWaiting(player)) { + return false; + } + + UUID id = player.getUniqueId(); + waiting.add(id); + + TimedInterruptListener listener = new TimedInterruptListener( + player, + arena, + () -> waiting.remove(id), + completed + ); + listener.runTaskLater(arena.getPlugin(), seconds * 20); + Bukkit.getPluginManager().registerEvents(listener, arena.getPlugin()); + + return true; + } + + static class TimedInterruptListener extends BukkitRunnable implements Listener { + Player player; + Location location; + Arena arena; + Runnable stopped; + Runnable completed; + + boolean done; + + TimedInterruptListener(Player player, Arena arena, Runnable stopped, Runnable completed) { + this.player = player; + this.location = player.getLocation(); + this.arena = arena; + this.stopped = stopped; + this.completed = completed; + + this.done = false; + } + + @Override + public void run() { + complete(); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void on(EntityDamageEvent event) { + if (done || !event.getEntity().equals(player)) { + return; + } + arena.getMessenger().tell(player, Msg.JOIN_INTERRUPTED_BY_DAMAGE); + interrupt(); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void on(PlayerMoveEvent event) { + if (done || !event.getPlayer().equals(player)) { + return; + } + if (location.getWorld() == event.getTo().getWorld() && location.distanceSquared(event.getTo()) < 1) { + return; + } + arena.getMessenger().tell(player, Msg.JOIN_INTERRUPTED_BY_MOVEMENT); + interrupt(); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void on(PlayerQuitEvent event) { + if (done || !event.getPlayer().equals(player)) { + return; + } + interrupt(); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void on(PlayerKickEvent event) { + if (done || !event.getPlayer().equals(player)) { + return; + } + interrupt(); + } + + void complete() { + if (done) { + return; + } + done = true; + + try { + HandlerList.unregisterAll(this); + stopped.run(); + completed.run(); + } finally { + clear(); + } + } + + void interrupt() { + if (done) { + return; + } + done = true; + + try { + HandlerList.unregisterAll(this); + stopped.run(); + super.cancel(); + } catch (IllegalStateException e) { + // Swallow this exception from super.cancel() + } finally { + clear(); + } + } + + void clear() { + player = null; + location = null; + arena = null; + stopped = null; + completed = null; + } + } +} diff --git a/src/main/resources/res/settings.yml b/src/main/resources/res/settings.yml index b341be6..d758209 100644 --- a/src/main/resources/res/settings.yml +++ b/src/main/resources/res/settings.yml @@ -20,6 +20,7 @@ share-items-in-arena: true min-players: 0 max-players: 0 max-join-distance: 0 +join-interrupt-timer: 0 first-wave-delay: 5 next-wave-delay: 0 wave-interval: 15