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
This commit is contained in:
Andreas Troelsen 2018-08-05 21:17:05 +02:00
parent c1375a31f5
commit 924a419b47
8 changed files with 240 additions and 34 deletions

View File

@ -12,6 +12,7 @@ These changes will (most likely) be included in the next version.
## [Unreleased] ## [Unreleased]
- It is now possible to add a fixed delay (in seconds) between waves with the new per-arena setting `next-wave-delay`. - 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. - 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. - Snow and ice no longer melts in arenas.

View File

@ -6,6 +6,7 @@ import static com.garbagemule.MobArena.util.config.ConfigUtils.parseLocation;
import com.garbagemule.MobArena.framework.Arena; import com.garbagemule.MobArena.framework.Arena;
import com.garbagemule.MobArena.framework.ArenaMaster; import com.garbagemule.MobArena.framework.ArenaMaster;
import com.garbagemule.MobArena.things.Thing; import com.garbagemule.MobArena.things.Thing;
import com.garbagemule.MobArena.util.JoinInterruptTimer;
import com.garbagemule.MobArena.util.config.ConfigUtils; import com.garbagemule.MobArena.util.config.ConfigUtils;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.Location; import org.bukkit.Location;
@ -44,6 +45,8 @@ public class ArenaMasterImpl implements ArenaMaster
private boolean enabled; private boolean enabled;
private JoinInterruptTimer joinInterruptTimer;
/** /**
* Default constructor. * Default constructor.
*/ */
@ -60,6 +63,8 @@ public class ArenaMasterImpl implements ArenaMaster
this.spawnsPets = new SpawnsPets(Material.BONE, Material.RAW_FISH); this.spawnsPets = new SpawnsPets(Material.BONE, Material.RAW_FISH);
this.enabled = config.getBoolean("global-settings.enabled", true); 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); return allowedCommands.contains(command);
} }
public JoinInterruptTimer getJoinInterruptTimer() {
return joinInterruptTimer;
}
/* /*
* ///////////////////////////////////////////////////////////////////////// * /////////////////////////////////////////////////////////////////////////
* // // Arena getters // * // // Arena getters //

View File

@ -30,6 +30,9 @@ public enum Msg {
JOIN_PLAYER_LIMIT_REACHED("The player limit of this arena has been reached."), JOIN_PLAYER_LIMIT_REACHED("The player limit of this arena has been reached."),
JOIN_STORE_INV_FAIL("Failed to store inventory. Try again."), JOIN_STORE_INV_FAIL("Failed to store inventory. Try again."),
JOIN_EXISTING_INV_RESTORED("Your old inventory items have been restored."), 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!"), JOIN_PLAYER_JOINED("You joined the arena. Have fun!"),
LEAVE_NOT_PLAYING("You are not in the arena."), 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."), LEAVE_NOT_READY("You did not ready up in time! Next time, ready up by clicking an iron block."),

View File

@ -1,5 +1,6 @@
package com.garbagemule.MobArena.commands.user; package com.garbagemule.MobArena.commands.user;
import com.garbagemule.MobArena.MobArena;
import com.garbagemule.MobArena.Msg; import com.garbagemule.MobArena.Msg;
import com.garbagemule.MobArena.commands.Command; import com.garbagemule.MobArena.commands.Command;
import com.garbagemule.MobArena.commands.CommandInfo; 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. // Run some rough sanity checks, and grab the arena to join.
Arena toArena = Commands.getArenaToJoinOrSpec(am, p, arg1); Arena toArena = Commands.getArenaToJoinOrSpec(am, p, arg1);
if (toArena == null) { if (toArena == null || !canJoin(p, toArena)) {
return true; 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! // 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());
}
} }
} }

View File

@ -1,5 +1,6 @@
package com.garbagemule.MobArena.commands.user; package com.garbagemule.MobArena.commands.user;
import com.garbagemule.MobArena.MobArena;
import com.garbagemule.MobArena.Msg; import com.garbagemule.MobArena.Msg;
import com.garbagemule.MobArena.commands.Command; import com.garbagemule.MobArena.commands.Command;
import com.garbagemule.MobArena.commands.CommandInfo; 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. // Run some rough sanity checks, and grab the arena to spec.
Arena toArena = Commands.getArenaToJoinOrSpec(am, p, arg1); Arena toArena = Commands.getArenaToJoinOrSpec(am, p, arg1);
if (toArena == null) { if (toArena == null || !canSpec(p, toArena)) {
return true; 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! // 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; 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());
}
}
} }

View File

@ -4,6 +4,7 @@ import com.garbagemule.MobArena.ArenaClass;
import com.garbagemule.MobArena.Messenger; import com.garbagemule.MobArena.Messenger;
import com.garbagemule.MobArena.MobArena; import com.garbagemule.MobArena.MobArena;
import com.garbagemule.MobArena.SpawnsPets; import com.garbagemule.MobArena.SpawnsPets;
import com.garbagemule.MobArena.util.JoinInterruptTimer;
import org.bukkit.Location; import org.bukkit.Location;
import org.bukkit.World; import org.bukkit.World;
import org.bukkit.entity.Entity; import org.bukkit.entity.Entity;
@ -88,6 +89,8 @@ public interface ArenaMaster
boolean isAllowed(String command); boolean isAllowed(String command);
JoinInterruptTimer getJoinInterruptTimer();
/*///////////////////////////////////////////////////////////////////////// /*/////////////////////////////////////////////////////////////////////////

View File

@ -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<UUID> 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;
}
}
}

View File

@ -20,6 +20,7 @@ share-items-in-arena: true
min-players: 0 min-players: 0
max-players: 0 max-players: 0
max-join-distance: 0 max-join-distance: 0
join-interrupt-timer: 0
first-wave-delay: 5 first-wave-delay: 5
next-wave-delay: 0 next-wave-delay: 0
wave-interval: 15 wave-interval: 15