Adds ability to require players to stand still for a command to execute

https://github.com/BentoBoxWorld/BentoBox/issues/837
This commit is contained in:
tastybento 2019-07-13 22:32:09 -07:00
parent 5fe4cccf7b
commit e284a6b57a
7 changed files with 371 additions and 75 deletions

View File

@ -128,6 +128,11 @@ public class Settings implements ConfigObject {
@ConfigEntry(path = "island.confirmation.time")
private int confirmationTime = 10;
// Timeout for team kick and leave commands
@ConfigComment("Time in seconds that players have to stand still before teleport commands activate, e.g. island go.")
@ConfigEntry(path = "island.delay.time")
private int delayTime = 0;
@ConfigComment("Ask the player to confirm the command he is using by typing it again.")
@ConfigEntry(path = "island.confirmation.commands.kick")
private boolean kickConfirmation = true;
@ -475,4 +480,15 @@ public class Settings implements ConfigObject {
public void setLogGithubDownloadData(boolean logGithubDownloadData) {
this.logGithubDownloadData = logGithubDownloadData;
}
public int getDelayTime() {
return delayTime;
}
/**
* @param delayTime the delayTime to set
*/
public void setDelayTime(int delayTime) {
this.delayTime = delayTime;
}
}

View File

@ -91,6 +91,11 @@ public abstract class ConfirmableCommand extends CompositeCommand {
askConfirmation(user, "", confirmed);
}
/**
* Holds the data to run once the confirmation is given
* @author tastybento
*
*/
private class Confirmer {
private final String topLabel;
private final String label;

View File

@ -0,0 +1,155 @@
package world.bentobox.bentobox.api.commands;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerMoveEvent;
import org.bukkit.scheduler.BukkitTask;
import world.bentobox.bentobox.api.addons.Addon;
import world.bentobox.bentobox.api.user.User;
/**
* BentoBox Delayed Teleport Command
* Adds ability to require the player stays still for a period of time before a command is executed
* @author tastybento
*/
public abstract class DelayedTeleportCommand extends CompositeCommand implements Listener {
/**
* User monitor map
*/
private static Map<UUID, DelayedCommand> toBeMonitored = new HashMap<>();
@EventHandler
public void onPlayerMove(PlayerMoveEvent e) {
UUID uuid = e.getPlayer().getUniqueId();
// Only check x,y,z
if (toBeMonitored.containsKey(uuid) && !e.getTo().toVector().equals(toBeMonitored.get(uuid).getLocation().toVector())) {
// Player moved
toBeMonitored.get(uuid).getTask().cancel();
toBeMonitored.remove(uuid);
// Player has another outstanding confirmation request that will now be cancelled
User.getInstance(e.getPlayer()).notify("commands.delay.moved-so-command-cancelled");
}
}
/**
* Top level command
* @param addon - addon creating the command
* @param label - string for this command
* @param aliases - aliases
*/
public DelayedTeleportCommand(Addon addon, String label, String... aliases) {
super(addon, label, aliases);
Bukkit.getPluginManager().registerEvents(this, getPlugin());
}
/**
* Command to register a command from an addon under a parent command (that could be from another addon)
* @param addon - this command's addon
* @param parent - parent command
* @param aliases - aliases for this command
*/
public DelayedTeleportCommand(Addon addon, CompositeCommand parent, String label, String... aliases ) {
super(addon, parent, label, aliases);
Bukkit.getPluginManager().registerEvents(this, getPlugin());
}
public DelayedTeleportCommand(CompositeCommand parent, String label, String... aliases) {
super(parent, label, aliases);
Bukkit.getPluginManager().registerEvents(this, getPlugin());
}
/**
* Tells user to stand still for a period of time before teleporting
* @param user User to tell
* @param message Optional message to send to the user to give them a bit more context. It must already be translated.
* @param confirmed Runnable to be executed if successfully delayed.
*/
public void delayCommand(User user, String message, Runnable confirmed) {
if (getSettings().getDelayTime() < 1) {
Bukkit.getScheduler().runTask(getPlugin(), confirmed);
return;
}
// Check for pending delays
UUID uuid = user.getUniqueId();
if (toBeMonitored.containsKey(uuid)) {
// A double request - clear out the old one
toBeMonitored.get(uuid).getTask().cancel();
toBeMonitored.remove(uuid);
// Player has another outstanding confirmation request that will now be cancelled
user.sendMessage("commands.delay.previous-command-cancelled");
}
// Send user the context message if it is not empty
if (!message.trim().isEmpty()) {
user.sendRawMessage(message);
}
// Tell user that they need to stand still
user.sendMessage("commands.delay.stand-still", "[seconds]", String.valueOf(getSettings().getDelayTime()));
// Set up the run task
BukkitTask task = Bukkit.getScheduler().runTaskLater(getPlugin(), () -> {
Bukkit.getScheduler().runTask(getPlugin(), toBeMonitored.get(uuid).getRunnable());
toBeMonitored.remove(uuid);
}, getPlugin().getSettings().getDelayTime() * 20L);
// Add to the monitor
toBeMonitored.put(uuid, new DelayedCommand(confirmed, task, user.getLocation()));
}
/**
* Tells user to stand still for a period of time before teleporting
* @param user User to monitor.
* @param command Runnable to be executed if player does not move.
*/
public void delayCommand(User user, Runnable command) {
delayCommand(user, "", command);
}
/**
* Holds the data to run once the confirmation is given
* @author tastybento
*
*/
private class DelayedCommand {
private final Runnable runnable;
private final BukkitTask task;
private final Location location;
/**
* @param label - command label
* @param runnable - runnable to run when confirmed
* @param task - task ID to cancel when confirmed
*/
DelayedCommand(Runnable runnable, BukkitTask task, Location location) {
this.runnable = runnable;
this.task = task;
this.location = location;
}
/**
* @return the runnable
*/
public Runnable getRunnable() {
return runnable;
}
/**
* @return the task
*/
public BukkitTask getTask() {
return task;
}
/**
* @return the location
*/
public Location getLocation() {
return location;
}
}
}

View File

@ -5,14 +5,14 @@ import java.util.List;
import org.apache.commons.lang.math.NumberUtils;
import world.bentobox.bentobox.api.commands.CompositeCommand;
import world.bentobox.bentobox.api.localization.TextVariables;
import world.bentobox.bentobox.api.commands.DelayedTeleportCommand;
import world.bentobox.bentobox.api.user.User;
import world.bentobox.bentobox.lists.Flags;
/**
* @author tastybento
*/
public class IslandGoCommand extends CompositeCommand {
public class IslandGoCommand extends DelayedTeleportCommand {
public IslandGoCommand(CompositeCommand islandCommand) {
super(islandCommand, "go", "home", "h");
@ -33,8 +33,8 @@ public class IslandGoCommand extends CompositeCommand {
return false;
}
if ((getIWM().inWorld(user.getWorld()) && Flags.PREVENT_TELEPORT_WHEN_FALLING.isSetForWorld(user.getWorld()))
&& user.getPlayer().getFallDistance() > 0) {
// We're sending the "hint" to the player to tell them they cannot teleport while falling.
&& user.getPlayer().getFallDistance() > 0) {
// We're sending the "hint" to the player to tell them they cannot teleport while falling.
user.sendMessage(Flags.PREVENT_TELEPORT_WHEN_FALLING.getHintReference());
return false;
}
@ -42,12 +42,11 @@ public class IslandGoCommand extends CompositeCommand {
int homeValue = Integer.parseInt(args.get(0));
int maxHomes = user.getPermissionValue(getPermissionPrefix() + "island.maxhomes", getIWM().getMaxHomes(getWorld()));
if (homeValue > 1 && homeValue <= maxHomes) {
getIslands().homeTeleport(getWorld(), user.getPlayer(), homeValue);
user.sendMessage("commands.island.go.tip", TextVariables.LABEL, getTopLabel());
this.delayCommand(user, () -> getIslands().homeTeleport(getWorld(), user.getPlayer(), homeValue));
return true;
}
}
getIslands().homeTeleport(getWorld(), user.getPlayer());
this.delayCommand(user, () -> getIslands().homeTeleport(getWorld(), user.getPlayer()));
return true;
}

View File

@ -78,6 +78,9 @@ island:
kick: true
leave: true
reset: true
delay:
# Time in seconds that players have to stand still before teleport commands activate, e.g. island go.
time: 0
name:
# These set the minimum and maximum size of a name.
min-length: 4

View File

@ -371,6 +371,10 @@ commands:
confirm: "&cType command again within &b[seconds]s&c to confirm."
previous-request-cancelled: "&6Previous confirmation request cancelled."
request-cancelled: "&cConfirmation timeout - &brequest cancelled."
delay:
previous-command-cancelled: "&cPrevious command cancelled"
stand-still: "&6Do not move! Teleporting in [seconds] seconds"
moved-so-command-cancelled: "&cYou moved. Teleport cancelled!"
island:
about:
description: "About this addon"
@ -379,7 +383,6 @@ commands:
description: "teleport you to your island"
teleport: "&aTeleporting you to your island."
teleported: "&aTeleported you to home &e#[number]."
tip: "&bType /[label] help &afor help."
help:
description: "The main island command"
pick-world: "&cSpecify world from [worlds]"

View File

@ -1,26 +1,35 @@
/**
*
*/
package world.bentobox.bentobox.api.commands.island;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.UUID;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.entity.Player;
import org.bukkit.event.player.PlayerMoveEvent;
import org.bukkit.plugin.PluginManager;
import org.bukkit.scheduler.BukkitScheduler;
import org.bukkit.scheduler.BukkitTask;
import org.bukkit.util.Vector;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.stubbing.Answer;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
@ -29,12 +38,14 @@ import org.powermock.reflect.Whitebox;
import world.bentobox.bentobox.BentoBox;
import world.bentobox.bentobox.Settings;
import world.bentobox.bentobox.api.commands.CompositeCommand;
import world.bentobox.bentobox.api.localization.TextVariables;
import world.bentobox.bentobox.api.user.Notifier;
import world.bentobox.bentobox.api.user.User;
import world.bentobox.bentobox.database.objects.Island;
import world.bentobox.bentobox.managers.CommandsManager;
import world.bentobox.bentobox.managers.IslandWorldManager;
import world.bentobox.bentobox.managers.IslandsManager;
import world.bentobox.bentobox.managers.LocalesManager;
import world.bentobox.bentobox.managers.PlaceholdersManager;
import world.bentobox.bentobox.managers.PlayersManager;
import world.bentobox.bentobox.util.Util;
@ -46,10 +57,24 @@ import world.bentobox.bentobox.util.Util;
@RunWith(PowerMockRunner.class)
@PrepareForTest({Bukkit.class, BentoBox.class, Util.class})
public class IslandGoCommandTest {
@Mock
private CompositeCommand ic;
private User user;
@Mock
private IslandsManager im;
@Mock
private Island island;
@Mock
private PluginManager pim;
@Mock
private Settings s;
@Mock
private BukkitTask task;
@Mock
private Player player;
private IslandGoCommand igc;
@Mock
private Notifier notifier;
/**
* @throws java.lang.Exception
@ -65,127 +90,217 @@ public class IslandGoCommandTest {
when(plugin.getCommandsManager()).thenReturn(cm);
// Settings
Settings s = mock(Settings.class);
when(plugin.getSettings()).thenReturn(s);
// Player
Player player = mock(Player.class);
// Sometimes use Mockito.withSettings().verboseLogging()
user = mock(User.class);
when(user.isOp()).thenReturn(false);
when(player.isOp()).thenReturn(false);
UUID uuid = UUID.randomUUID();
when(user.getUniqueId()).thenReturn(uuid);
when(user.getPlayer()).thenReturn(player);
when(user.getName()).thenReturn("tastybento");
when(player.getUniqueId()).thenReturn(uuid);
when(player.getName()).thenReturn("tastybento");
user = User.getInstance(player);
// Set the User class plugin as this one
User.setPlugin(plugin);
// Parent command has no aliases
ic = mock(CompositeCommand.class);
when(ic.getSubCommandAliases()).thenReturn(new HashMap<>());
when(ic.getTopLabel()).thenReturn("island");
// No island for player to begin with (set it later in the tests)
im = mock(IslandsManager.class);
when(im.hasIsland(Mockito.any(), Mockito.eq(uuid))).thenReturn(false);
when(im.isOwner(Mockito.any(), Mockito.eq(uuid))).thenReturn(false);
when(im.hasIsland(any(), eq(uuid))).thenReturn(false);
when(im.isOwner(any(), eq(uuid))).thenReturn(false);
when(plugin.getIslands()).thenReturn(im);
// Has team
PlayersManager pm = mock(PlayersManager.class);
when(im.inTeam(Mockito.any(), Mockito.eq(uuid))).thenReturn(true);
when(im.inTeam(any(), eq(uuid))).thenReturn(true);
when(plugin.getPlayers()).thenReturn(pm);
// Server & Scheduler
BukkitScheduler sch = mock(BukkitScheduler.class);
PowerMockito.mockStatic(Bukkit.class);
when(Bukkit.getScheduler()).thenReturn(sch);
when(sch.runTaskLater(any(), any(Runnable.class), any(Long.class))).thenReturn(task);
// Event register
when(Bukkit.getPluginManager()).thenReturn(pim);
// Island Banned list initialization
island = mock(Island.class);
when(island.getBanned()).thenReturn(new HashSet<>());
when(island.isBanned(Mockito.any())).thenReturn(false);
when(im.getIsland(Mockito.any(), Mockito.any(UUID.class))).thenReturn(island);
when(island.isBanned(any())).thenReturn(false);
when(im.getIsland(any(), any(UUID.class))).thenReturn(island);
// IWM friendly name
IslandWorldManager iwm = mock(IslandWorldManager.class);
when(iwm.getFriendlyName(Mockito.any())).thenReturn("BSkyBlock");
when(iwm.getFriendlyName(any())).thenReturn("BSkyBlock");
when(plugin.getIWM()).thenReturn(iwm);
// Number of homes
PowerMockito.mockStatic(Util.class);
// 1 home for now
when(user.getPermissionValue(Mockito.anyString(), Mockito.anyInt())).thenReturn(1);
// Locales
LocalesManager lm = mock(LocalesManager.class);
when(lm.get(Mockito.any(), Mockito.any())).thenAnswer((Answer<String>) invocation -> invocation.getArgumentAt(1, String.class));
when(plugin.getLocalesManager()).thenReturn(lm);
// Return the same string
PlaceholdersManager phm = mock(PlaceholdersManager.class);
when(phm.replacePlaceholders(any(), anyString())).thenAnswer((Answer<String>) invocation -> invocation.getArgumentAt(1, String.class));
when(plugin.getPlaceholdersManager()).thenReturn(phm);
// Notifier
when(plugin.getNotifier()).thenReturn(notifier);
// Command
igc = new IslandGoCommand(ic);
}
@After
public void tearDown() {
User.clearUsers();
}
/**
* Test method for .
* Test method for {@link IslandGoCommand#execute(User, String, List)}
*/
@Test
public void testExecuteNoArgsNoIsland() {
when(im.getIsland(Mockito.any(), Mockito.any(UUID.class))).thenReturn(null);
IslandGoCommand igc = new IslandGoCommand(ic);
assertFalse(igc.execute(user, igc.getLabel(), new ArrayList<>()));
Mockito.verify(user).sendMessage("general.errors.no-island");
when(im.getIsland(any(), any(UUID.class))).thenReturn(null);
assertFalse(igc.execute(user, igc.getLabel(), Collections.emptyList()));
verify(player).sendMessage("general.errors.no-island");
}
/**
* Test method for .
* Test method for {@link IslandGoCommand#execute(User, String, List)}
*/
@Test
public void testExecuteNoArgs() {
when(im.getIsland(Mockito.any(), Mockito.any(UUID.class))).thenReturn(island);
IslandGoCommand igc = new IslandGoCommand(ic);
assertTrue(igc.execute(user, igc.getLabel(), new ArrayList<>()));
when(im.getIsland(any(), any(UUID.class))).thenReturn(island);
assertTrue(igc.execute(user, igc.getLabel(), Collections.emptyList()));
}
/**
* Test method for .
* Test method for {@link IslandGoCommand#execute(User, String, List)}
*/
@Test
public void testExecuteNoArgsMultipleHomes() {
when(im.getIsland(Mockito.any(), Mockito.any(UUID.class))).thenReturn(island);
when(user.getPermissionValue(Mockito.anyString(), Mockito.anyInt())).thenReturn(3);
IslandGoCommand igc = new IslandGoCommand(ic);
assertTrue(igc.execute(user, igc.getLabel(), new ArrayList<>()));
when(im.getIsland(any(), any(UUID.class))).thenReturn(island);
//when(user.getPermissionValue(anyString(), anyInt())).thenReturn(3);
assertTrue(igc.execute(user, igc.getLabel(), Collections.emptyList()));
}
/**
* Test method for .
* Test method for {@link IslandGoCommand#execute(User, String, List)}
*/
@Test
public void testExecuteArgs1MultipleHomes() {
when(im.getIsland(Mockito.any(), Mockito.any(UUID.class))).thenReturn(island);
when(user.getPermissionValue(Mockito.anyString(), Mockito.anyInt())).thenReturn(3);
IslandGoCommand igc = new IslandGoCommand(ic);
List<String> args = new ArrayList<>();
args.add("1");
assertTrue(igc.execute(user, igc.getLabel(), args));
when(im.getIsland(any(), any(UUID.class))).thenReturn(island);
//when(user.getPermissionValue(anyString(), anyInt())).thenReturn(3);
assertTrue(igc.execute(user, igc.getLabel(), Collections.singletonList("1")));
}
/**
* Test method for .
* Test method for {@link IslandGoCommand#execute(User, String, List)}
*/
@Test
public void testExecuteArgs2MultipleHomes() {
when(im.getIsland(Mockito.any(), Mockito.any(UUID.class))).thenReturn(island);
when(user.getPermissionValue(Mockito.anyString(), Mockito.anyInt())).thenReturn(3);
IslandGoCommand igc = new IslandGoCommand(ic);
List<String> args = new ArrayList<>();
args.add("2");
assertTrue(igc.execute(user, igc.getLabel(), args));
Mockito.verify(user).sendMessage("commands.island.go.tip", TextVariables.LABEL, "island");
when(im.getIsland(any(), any(UUID.class))).thenReturn(island);
//when(user.getPermissionValue(anyString(), anyInt())).thenReturn(3);
assertTrue(igc.execute(user, igc.getLabel(), Collections.singletonList("2")));
}
/**
* Test method for .
* Test method for {@link IslandGoCommand#execute(User, String, List)}
*/
@Test
public void testExecuteArgsJunkMultipleHomes() {
when(im.getIsland(Mockito.any(), Mockito.any(UUID.class))).thenReturn(island);
when(user.getPermissionValue(Mockito.anyString(), Mockito.anyInt())).thenReturn(3);
IslandGoCommand igc = new IslandGoCommand(ic);
List<String> args = new ArrayList<>();
args.add("sdfsdf");
assertTrue(igc.execute(user, igc.getLabel(), args));
when(im.getIsland(any(), any(UUID.class))).thenReturn(island);
//when(user.getPermissionValue(anyString(), anyInt())).thenReturn(3);
assertTrue(igc.execute(user, igc.getLabel(), Collections.singletonList("sdfghhj")));
}
/**
* Test method for {@link IslandGoCommand#execute(User, String, List)}
*/
@Test
public void testExecuteNoArgsDelay() {
when(s.getDelayTime()).thenReturn(10);
when(im.getIsland(any(), any(UUID.class))).thenReturn(island);
assertTrue(igc.execute(user, igc.getLabel(), Collections.emptyList()));
verify(player).sendMessage(eq("commands.delay.stand-still"));
}
/**
* Test method for {@link IslandGoCommand#execute(User, String, List)}
*/
@Test
public void testExecuteNoArgsDelayTwice() {
when(s.getDelayTime()).thenReturn(10);
when(im.getIsland(any(), any(UUID.class))).thenReturn(island);
assertTrue(igc.execute(user, igc.getLabel(), Collections.emptyList()));
// Twice
assertTrue(igc.execute(user, igc.getLabel(), Collections.emptyList()));
verify(task).cancel();
verify(player).sendMessage(eq("commands.delay.previous-command-cancelled"));
verify(player, Mockito.times(2)).sendMessage(eq("commands.delay.stand-still"));
}
/**
* Test method for {@link IslandGoCommand#execute(User, String, List)}
*/
@Test
public void testExecuteNoArgsDelayMultiHome() {
when(im.getIsland(any(), any(UUID.class))).thenReturn(island);
//when(user.getPermissionValue(anyString(), anyInt())).thenReturn(3);
when(s.getDelayTime()).thenReturn(10);
when(im.getIsland(any(), any(UUID.class))).thenReturn(island);
assertTrue(igc.execute(user, igc.getLabel(), Collections.singletonList("2")));
verify(player).sendMessage(eq("commands.delay.stand-still"));
}
/**
* Test method for {@link IslandGoCommand#onPlayerMove(PlayerMoveEvent)}
*/
@Test
public void testOnPlayerMoveHeadMoveNothing() {
Location l = mock(Location.class);
Vector vector = mock(Vector.class);
when(l.toVector()).thenReturn(vector);
when(player.getLocation()).thenReturn(l);
PlayerMoveEvent e = new PlayerMoveEvent(player, l, l);
igc.onPlayerMove(e);
verify(player, Mockito.never()).sendMessage(eq("commands.delay.moved-so-command-cancelled"));
}
/**
* Test method for {@link IslandGoCommand#onPlayerMove(PlayerMoveEvent)}
*/
@Test
public void testOnPlayerMoveHeadMoveTeleportPending() {
Location l = mock(Location.class);
Vector vector = mock(Vector.class);
when(l.toVector()).thenReturn(vector);
when(player.getLocation()).thenReturn(l);
testExecuteNoArgsDelay();
PlayerMoveEvent e = new PlayerMoveEvent(player, l, l);
igc.onPlayerMove(e);
verify(player, Mockito.never()).sendMessage(eq("commands.delay.moved-so-command-cancelled"));
}
/**
* Test method for {@link IslandGoCommand#onPlayerMove(PlayerMoveEvent)}
*/
@Test
public void testOnPlayerMovePlayerMoveTeleportPending() {
Location l = mock(Location.class);
Vector vector = mock(Vector.class);
when(l.toVector()).thenReturn(vector);
when(player.getLocation()).thenReturn(l);
testExecuteNoArgsDelay();
Location l2 = mock(Location.class);
Vector vector2 = mock(Vector.class);
when(l2.toVector()).thenReturn(vector2);
PlayerMoveEvent e = new PlayerMoveEvent(player, l, l2);
igc.onPlayerMove(e);
verify(notifier).notify(any(), eq("commands.delay.moved-so-command-cancelled"));
}
}