diff --git a/config.yml b/config.yml index 94c86b4c4..a98fffd72 100644 --- a/config.yml +++ b/config.yml @@ -98,6 +98,9 @@ general: acid-blocked-commands: - home + # Time in seconds that players have to confirm sensitive commands, e.g. island reset + confimation-time: 20 + ### World Settings ### world: # Name of the world - if it does not exist then it will be generated. diff --git a/locales/en-US.yml b/locales/en-US.yml index b8d814b8b..71bdd7d4f 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -34,6 +34,7 @@ general: unknown-command: "&cUnknown command. Do &b/[label] help &cfor help." warp-not-safe: "&cThat warp is not safe right now!" wrong-world: "&cYou are not in the right world to do that!" + you-must-wait: "&cYou must wait [seconds]s before you can do that command again" tips: changing-obsidian-to-lava: "Changing obsidian back into lava. Be careful!" @@ -80,6 +81,9 @@ commands: reset: description: "restart your island and remove the old one" must-remove-members: "You must remove all members from your island before you can restart it (/island kick )." + none-left: "&cYou have no more resets left!" + resets-left: "&cYou have [number] resets left" + confirm: "&cType [label] reset confirm within [seconds]s to confirm reset" sethome: description: "set your teleport point for /island" must-be-on-your-island: "You must be on your island to set home!" diff --git a/src/main/java/us/tastybento/bskyblock/Settings.java b/src/main/java/us/tastybento/bskyblock/Settings.java index 560780d01..dc6e15a88 100644 --- a/src/main/java/us/tastybento/bskyblock/Settings.java +++ b/src/main/java/us/tastybento/bskyblock/Settings.java @@ -14,6 +14,7 @@ import org.bukkit.inventory.ItemStack; import org.bukkit.potion.PotionEffectType; import us.tastybento.bskyblock.Constants.GameType; +import us.tastybento.bskyblock.api.configuration.ConfigComment; import us.tastybento.bskyblock.api.configuration.ConfigEntry; import us.tastybento.bskyblock.api.configuration.ISettings; import us.tastybento.bskyblock.api.configuration.StoreAt; @@ -78,6 +79,10 @@ public class Settings implements ISettings { @ConfigEntry(path = "general.allow-obsidian-scooping") private boolean allowObsidianScooping = true; + + @ConfigComment("Time in seconds that players have to confirm sensitive commands, e.g. island reset") + @ConfigEntry(path = "general.confirmation-time") + private int confirmationTime = 20; // --------------------------------------------- @@ -1242,6 +1247,18 @@ public class Settings implements ISettings { public void setFakePlayers(Set fakePlayers) { this.fakePlayers = fakePlayers; } + /** + * @return the confirmationTime + */ + public int getConfirmationTime() { + return confirmationTime; + } + /** + * @param confirmationTime the confirmationTime to set + */ + public void setConfirmationTime(int confirmationTime) { + this.confirmationTime = confirmationTime; + } } \ No newline at end of file diff --git a/src/main/java/us/tastybento/bskyblock/api/commands/CompositeCommand.java b/src/main/java/us/tastybento/bskyblock/api/commands/CompositeCommand.java index d85adca2a..b8032daa3 100644 --- a/src/main/java/us/tastybento/bskyblock/api/commands/CompositeCommand.java +++ b/src/main/java/us/tastybento/bskyblock/api/commands/CompositeCommand.java @@ -127,7 +127,7 @@ public abstract class CompositeCommand extends Command implements PluginIdentifi subCommandAliases = new LinkedHashMap<>(); // Add aliases to the parent for this command for (String alias : aliases) { - parent.subCommandAliases.put(alias, this); + parent.getSubCommandAliases().put(alias, this); } setUsage(""); setup(); @@ -434,4 +434,11 @@ public abstract class CompositeCommand extends Command implements PluginIdentifi protected boolean showHelp(CompositeCommand command, User user) { return command.getSubCommand("help").map(helpCommand -> helpCommand.execute(user, new ArrayList<>())).orElse(false); } + + /** + * @return the subCommandAliases + */ + public Map getSubCommandAliases() { + return subCommandAliases; + } } diff --git a/src/main/java/us/tastybento/bskyblock/commands/island/IslandResetCommand.java b/src/main/java/us/tastybento/bskyblock/commands/island/IslandResetCommand.java index b7f917ad4..e6d9eb4b8 100644 --- a/src/main/java/us/tastybento/bskyblock/commands/island/IslandResetCommand.java +++ b/src/main/java/us/tastybento/bskyblock/commands/island/IslandResetCommand.java @@ -1,8 +1,14 @@ package us.tastybento.bskyblock.commands.island; import java.io.IOException; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import org.bukkit.Bukkit; import org.bukkit.GameMode; import org.bukkit.entity.Player; @@ -15,7 +21,8 @@ import us.tastybento.bskyblock.managers.island.NewIsland; public class IslandResetCommand extends CompositeCommand { - private static final boolean DEBUG = false; + private Map cooldown; + private Set confirm; public IslandResetCommand(CompositeCommand islandCommand) { super(islandCommand, "reset", "restart"); @@ -23,6 +30,8 @@ public class IslandResetCommand extends CompositeCommand { @Override public void setup() { + cooldown = new HashMap<>(); + confirm = new HashSet<>(); setPermission(Constants.PERMPREFIX + "island.create"); setOnlyPlayer(true); setDescription("commands.island.reset.description"); @@ -30,36 +39,71 @@ public class IslandResetCommand extends CompositeCommand { @Override public boolean execute(User user, List args) { + // Check cooldown + if (getSettings().getResetWait() > 0 && onRestartWaitTime(user) > 0 && !user.isOp()) { + user.sendMessage("general.errors.you-must-wait", "[seconds]", String.valueOf(onRestartWaitTime(user))); + return false; + } if (!getIslands().hasIsland(user.getUniqueId())) { user.sendMessage("general.errors.no-island"); - return true; + return false; } if (!getIslands().isOwner(user.getUniqueId())) { user.sendMessage("general.errors.not-leader"); return false; } - if (getPlugin().getPlayers().inTeam(user.getUniqueId())) { + if (getPlayers().inTeam(user.getUniqueId())) { user.sendMessage("commands.island.reset.must-remove-members"); + return false; + } + if (getSettings().getResetLimit() >= 0 ) { + if (getPlayers().getResetsLeft(user.getUniqueId()) == 0) { + user.sendMessage("commands.island.reset.none-left"); + return false; + } else { + // Notify how many resets are left + user.sendMessage("commands.island.reset.resets-left", "[number]", String.valueOf(getPlayers().getResetsLeft(user.getUniqueId()))); + } + } + // Check confirmation or reset immediately if no confirmation required + if (!getSettings().isResetConfirmation() || (confirm.contains(user.getUniqueId()) && args.size() == 1 && args.get(0).equalsIgnoreCase("confirm"))) { + // Reset the island + Player player = user.getPlayer(); + player.setGameMode(GameMode.SPECTATOR); + // Get the player's old island + Island oldIsland = getIslands().getIsland(player.getUniqueId()); + // Remove them from this island (it still exists and will be deleted later) + getIslands().removePlayer(player.getUniqueId()); + // Create new island and then delete the old one + try { + NewIsland.builder() + .player(player) + .reason(Reason.RESET) + .oldIsland(oldIsland) + .build(); + } catch (IOException e) { + getPlugin().logError("Could not create island for player. " + e.getMessage()); + user.sendMessage("commands.island.create.unable-create-island"); + } + setCooldown(user); + return true; + } else { + // Require confirmation + user.sendMessage("commands.island.reset.confirm", "[label]", Constants.ISLANDCOMMAND, "[seconds]", String.valueOf(getSettings().getConfirmationTime())); + confirm.add(user.getUniqueId()); + Bukkit.getScheduler().runTaskLater(getPlugin(), () -> confirm.remove(user.getUniqueId()), getSettings().getConfirmationTime() * 20L); return true; } - Player player = user.getPlayer(); - player.setGameMode(GameMode.SPECTATOR); - // Get the player's old island - Island oldIsland = getIslands().getIsland(player.getUniqueId()); - // Remove them from this island (it still exists and will be deleted later) - getIslands().removePlayer(player.getUniqueId()); - // Create new island and then delete the old one - try { - NewIsland.builder() - .player(player) - .reason(Reason.RESET) - .oldIsland(oldIsland) - .build(); - } catch (IOException e) { - getPlugin().logError("Could not create island for player. " + e.getMessage()); - user.sendMessage("commands.island.create.unable-create-island"); - } - return true; } + private int onRestartWaitTime(User user) { + if (!cooldown.containsKey(user.getUniqueId())) { + return 0; + } + return (int) ((System.currentTimeMillis() - cooldown.get(user.getUniqueId()) / 1000)); + } + + private void setCooldown(User user) { + cooldown.put(user.getUniqueId(), System.currentTimeMillis() + (getSettings().getResetLimit() * 1000L)); + } } diff --git a/src/test/java/us/tastybento/bskyblock/commands/IslandCommandTest.java b/src/test/java/us/tastybento/bskyblock/commands/IslandCommandTest.java index 9a6816331..af5adb55a 100644 --- a/src/test/java/us/tastybento/bskyblock/commands/IslandCommandTest.java +++ b/src/test/java/us/tastybento/bskyblock/commands/IslandCommandTest.java @@ -34,7 +34,7 @@ import us.tastybento.bskyblock.managers.IslandsManager; import us.tastybento.bskyblock.managers.PlayersManager; @RunWith(PowerMockRunner.class) -@PrepareForTest( { BSkyBlock.class }) +@PrepareForTest(BSkyBlock.class) public class IslandCommandTest { @Mock diff --git a/src/test/java/us/tastybento/bskyblock/commands/island/IslandResetCommandTest.java b/src/test/java/us/tastybento/bskyblock/commands/island/IslandResetCommandTest.java new file mode 100644 index 000000000..fc66f6018 --- /dev/null +++ b/src/test/java/us/tastybento/bskyblock/commands/island/IslandResetCommandTest.java @@ -0,0 +1,158 @@ +/** + * + */ +package us.tastybento.bskyblock.commands.island; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.UUID; + +import org.bukkit.entity.Player; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.powermock.api.mockito.PowerMockito; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; +import org.powermock.reflect.Whitebox; + +import us.tastybento.bskyblock.BSkyBlock; +import us.tastybento.bskyblock.Settings; +import us.tastybento.bskyblock.api.user.User; +import us.tastybento.bskyblock.commands.IslandCommand; +import us.tastybento.bskyblock.database.objects.Island; +import us.tastybento.bskyblock.managers.CommandsManager; +import us.tastybento.bskyblock.managers.IslandsManager; +import us.tastybento.bskyblock.managers.PlayersManager; +import us.tastybento.bskyblock.managers.island.NewIsland; + +/** + * @author tastybento + * + */ +@RunWith(PowerMockRunner.class) +@PrepareForTest({BSkyBlock.class, NewIsland.class }) +public class IslandResetCommandTest { + + private static BSkyBlock plugin; + + /** + * @throws java.lang.Exception + */ + @BeforeClass + public static void setUpBeforeClass() throws Exception { + plugin = mock(BSkyBlock.class); + Whitebox.setInternalState(BSkyBlock.class, "instance", plugin); + /* + NewIsland.Builder builder = mock(NewIsland.Builder.class); + when(builder.player(Mockito.any())).thenReturn(builder); + when(builder.oldIsland(Mockito.any())).thenReturn(builder); + when(builder.reason(Mockito.any())).thenReturn(builder); + when(builder.build()).thenReturn(mock(Island.class)); + PowerMockito.mockStatic(NewIsland.class); + when(NewIsland.builder()).thenReturn(builder); + */ + } + + /** + * @throws java.lang.Exception + */ + @Before + public void setUp() throws Exception { + CommandsManager cm = mock(CommandsManager.class); + when(plugin.getCommandsManager()).thenReturn(cm); + } + + /** + * Test method for {@link us.tastybento.bskyblock.commands.island.IslandResetCommand#execute(us.tastybento.bskyblock.api.user.User, java.util.List)}. + * @throws IOException + */ + @Test + public void testExecuteUserListOfString() throws IOException { + Settings s = mock(Settings.class); + when(s.getResetWait()).thenReturn(0L); + when(plugin.getSettings()).thenReturn(s); + + Player p = mock(Player.class); + User user = mock(User.class, Mockito.withSettings().verboseLogging()); + when(user.isOp()).thenReturn(false); + UUID uuid = UUID.randomUUID(); + when(user.getUniqueId()).thenReturn(uuid); + when(user.getPlayer()).thenReturn(p); + + IslandCommand ic = mock(IslandCommand.class); + when(ic.getSubCommandAliases()).thenReturn(new HashMap<>()); + + IslandResetCommand irc = new IslandResetCommand(ic); + + // No island + IslandsManager im = mock(IslandsManager.class); + when(im.hasIsland(Mockito.eq(uuid))).thenReturn(false); + when(im.isOwner(Mockito.eq(uuid))).thenReturn(false); + when(plugin.getIslands()).thenReturn(im); + + // Has team + PlayersManager pm = mock(PlayersManager.class); + when(pm.inTeam(Mockito.eq(uuid))).thenReturn(true); + when(plugin.getPlayers()).thenReturn(pm); + + // Test the reset command + // Does not have island + assertFalse(irc.execute(user, new ArrayList<>())); + Mockito.verify(user).sendMessage("general.errors.no-island"); + + // Now has island, but is not the leader + when(im.hasIsland(Mockito.eq(uuid))).thenReturn(true); + assertFalse(irc.execute(user, new ArrayList<>())); + Mockito.verify(user).sendMessage("general.errors.not-leader"); + + // Now is owner, but still has team + when(im.isOwner(Mockito.eq(uuid))).thenReturn(true); + assertFalse(irc.execute(user, new ArrayList<>())); + Mockito.verify(user).sendMessage("commands.island.reset.must-remove-members"); + + // Now has no team + when(pm.inTeam(Mockito.eq(uuid))).thenReturn(false); + + // Block based on no resets left + when(s.getResetLimit()).thenReturn(1); + when(pm.getResetsLeft(Mockito.eq(uuid))).thenReturn(0); + assertFalse(irc.execute(user, new ArrayList<>())); + Mockito.verify(user).sendMessage("commands.island.reset.none-left"); + + // Give the user some resets + when(pm.getResetsLeft(Mockito.eq(uuid))).thenReturn(1); + + // No confirmation required + when(s.isResetConfirmation()).thenReturn(false); + + // Old island + Island oldIsland = mock(Island.class); + when(im.getIsland(Mockito.eq(uuid))).thenReturn(oldIsland); + + // Mock up NewIsland + NewIsland.Builder builder = mock(NewIsland.Builder.class); + when(builder.player(Mockito.any())).thenReturn(builder); + when(builder.oldIsland(Mockito.any())).thenReturn(builder); + when(builder.reason(Mockito.any())).thenReturn(builder); + when(builder.build()).thenReturn(mock(Island.class)); + PowerMockito.mockStatic(NewIsland.class); + when(NewIsland.builder()).thenReturn(builder); + + // Reset + assertTrue(irc.execute(user, new ArrayList<>())); + Mockito.verify(builder).build(); + Mockito.verify(user).sendMessage("commands.island.reset.resets-left", "[number]", "1"); + + + + } +}