diff --git a/src/main/java/world/bentobox/bentobox/api/commands/admin/team/AdminTeamKickCommand.java b/src/main/java/world/bentobox/bentobox/api/commands/admin/team/AdminTeamKickCommand.java index 3548f9fcf..5a58d678c 100644 --- a/src/main/java/world/bentobox/bentobox/api/commands/admin/team/AdminTeamKickCommand.java +++ b/src/main/java/world/bentobox/bentobox/api/commands/admin/team/AdminTeamKickCommand.java @@ -1,7 +1,12 @@ package world.bentobox.bentobox.api.commands.admin.team; +import java.util.ArrayList; import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; import java.util.UUID; +import java.util.stream.Collectors; import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.Nullable; @@ -22,6 +27,7 @@ public class AdminTeamKickCommand extends CompositeCommand { private @Nullable UUID targetUUID; + private @Nullable Island island; public AdminTeamKickCommand(CompositeCommand parent) { super(parent, "kick"); @@ -37,7 +43,7 @@ public void setup() { @Override public boolean canExecute(User user, String label, List args) { // If args are not right, show help - if (args.size() != 1) { + if (args.isEmpty() || args.size() > 2) { showHelp(this, user); return false; } @@ -53,34 +59,74 @@ public boolean canExecute(User user, String label, List args) { return false; } - return true; - } - - @Override - public boolean execute(User user, String label, @NonNull List args) { - List islands = getIslands().getIslands(getWorld(), targetUUID); + Map islands = getMemberIslandsXYZ(targetUUID); if (islands.isEmpty()) { + user.sendMessage("commands.admin.team.kick.not-in-team"); return false; } - islands.forEach(island -> { - if (!user.getUniqueId().equals(island.getOwner())) { - assert targetUUID != null; - User target = User.getInstance(targetUUID); - target.sendMessage("commands.admin.team.kick.admin-kicked"); - getIslands().removePlayer(island, targetUUID); - user.sendMessage("commands.admin.team.kick.success", TextVariables.NAME, target.getName(), "[owner]", - getPlayers().getName(island.getOwner())); - // Fire event so add-ons know - TeamEvent.builder().island(island).reason(TeamEvent.Reason.KICK).involvedPlayer(targetUUID).admin(true) - .build(); - IslandEvent.builder().island(island).involvedPlayer(targetUUID).admin(true) - .reason(IslandEvent.Reason.RANK_CHANGE) - .rankChange(island.getRank(target), RanksManager.VISITOR_RANK).build(); + if (args.size() == 1) { + if (islands.size() == 1) { + island = islands.values().iterator().next(); + } else { + // Multiple islands – require the player to specify which one + user.sendMessage("commands.admin.unregister.errors.player-has-more-than-one-island"); + islands.keySet().forEach(coords -> + user.sendMessage("commands.admin.unregister.errors.specify-island-location", + TextVariables.XYZ, coords)); + return false; + } + } else { + // args.size() == 2: xyz was supplied + if (!islands.containsKey(args.get(1))) { + user.sendMessage("commands.admin.unregister.errors.unknown-island-location"); + return false; } - }); - user.sendMessage("commands.admin.team.kick.success-all"); + island = islands.get(args.get(1)); + } + return true; + } + /** + * Returns a map of x,y,z → island for all team islands in this world that the + * target player is a member of. + */ + private Map getMemberIslandsXYZ(UUID target) { + return getIslands().getIslands(getWorld(), target).stream() + .filter(Island::hasTeam) + .collect(Collectors.toMap(i -> Util.xyz(i.getCenter().toVector()), i -> i)); + } + + @Override + public boolean execute(User user, String label, @NonNull List args) { + Objects.requireNonNull(island); + Objects.requireNonNull(targetUUID); + User target = User.getInstance(targetUUID); + target.sendMessage("commands.admin.team.kick.admin-kicked"); + getIslands().removePlayer(island, targetUUID); + user.sendMessage("commands.admin.team.kick.success", TextVariables.NAME, target.getName(), "[owner]", + getPlayers().getName(island.getOwner())); + // Fire events so add-ons know + TeamEvent.builder().island(island).reason(TeamEvent.Reason.KICK).involvedPlayer(targetUUID).admin(true).build(); + IslandEvent.builder().island(island).involvedPlayer(targetUUID).admin(true) + .reason(IslandEvent.Reason.RANK_CHANGE) + .rankChange(island.getRank(target), RanksManager.VISITOR_RANK).build(); return true; } + + @Override + public Optional> tabComplete(User user, String alias, List args) { + String lastArg = !args.isEmpty() ? args.getLast() : ""; + if (args.isEmpty()) { + // Don't show every player on the server. Require at least the first letter + return Optional.empty(); + } else if (args.size() == 2) { + // Completing the xyz arg: show the islands the target is a member of + UUID targetId = getPlayers().getUUID(args.getFirst()); + if (targetId != null) { + return Optional.of(Util.tabLimit(new ArrayList<>(getMemberIslandsXYZ(targetId).keySet()), lastArg)); + } + } + return Optional.empty(); + } } diff --git a/src/main/resources/locales/en-US.yml b/src/main/resources/locales/en-US.yml index d6bebdb7b..0921adefc 100644 --- a/src/main/resources/locales/en-US.yml +++ b/src/main/resources/locales/en-US.yml @@ -157,13 +157,12 @@ commands: fixed: 'Fixed' done: 'Scan' kick: - parameters: + parameters: [x,y,z] description: kick a player from a team cannot-kick-owner: 'You cannot kick the owner. Kick members first.' not-in-team: 'This player is not in a team.' admin-kicked: 'The admin kicked you from the team.' success: '[name] has been kicked from [owner]''s [prefix_island].' - success-all: 'Player removed from all teams in this world' setowner: parameters: description: transfers [prefix_island] ownership to the player diff --git a/src/test/java/world/bentobox/bentobox/api/commands/admin/team/AdminTeamKickCommandTest.java b/src/test/java/world/bentobox/bentobox/api/commands/admin/team/AdminTeamKickCommandTest.java index b114e8da8..8b3c33233 100644 --- a/src/test/java/world/bentobox/bentobox/api/commands/admin/team/AdminTeamKickCommandTest.java +++ b/src/test/java/world/bentobox/bentobox/api/commands/admin/team/AdminTeamKickCommandTest.java @@ -5,7 +5,6 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -17,6 +16,7 @@ import java.util.Optional; import java.util.UUID; +import org.bukkit.util.Vector; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -48,6 +48,8 @@ class AdminTeamKickCommandTest extends CommonTestSetup { @Mock private Island island2; + private static final String XYZ = "0,0,0"; + @Override @BeforeEach public void setUp() throws Exception { @@ -58,8 +60,9 @@ public void setUp() throws Exception { CommandsManager cm = mock(CommandsManager.class); when(plugin.getCommandsManager()).thenReturn(cm); - // Player + // Admin player (user) when(user.isOp()).thenReturn(false); + when(user.isPlayer()).thenReturn(true); uuid = UUID.randomUUID(); notUUID = UUID.randomUUID(); while (notUUID.equals(uuid)) { @@ -74,19 +77,20 @@ public void setUp() throws Exception { when(ac.getSubCommandAliases()).thenReturn(new HashMap<>()); when(ac.getWorld()).thenReturn(world); - // Island - when(island.getOwner()).thenReturn(uuid); + // island2 is owned by notUUID (the target) and has a team when(island2.getOwner()).thenReturn(notUUID); + when(island2.hasTeam()).thenReturn(true); + when(island2.getCenter()).thenReturn(location); + when(location.toVector()).thenReturn(new Vector(0, 0, 0)); - // Player has island to begin with - when(im.hasIsland(any(), any(UUID.class))).thenReturn(true); - when(im.hasIsland(any(), any(User.class))).thenReturn(true); - when(im.getIslands(world, uuid)).thenReturn(List.of(island, island2)); - - // Has team - when(im.inTeam(any(), eq(uuid))).thenReturn(true); + // Target (notUUID) is in a team and is a member of island2 + when(im.inTeam(any(), eq(notUUID))).thenReturn(true); + // By default, target is on island2 only + when(im.getIslands(world, notUUID)).thenReturn(List.of(island2)); when(plugin.getPlayers()).thenReturn(pm); + when(pm.getUUID(any())).thenReturn(notUUID); + when(pm.getName(any())).thenReturn("target"); // Locales LocalesManager lm = mock(LocalesManager.class); @@ -95,7 +99,6 @@ public void setUp() throws Exception { // Addon when(iwm.getAddon(any())).thenReturn(Optional.empty()); - } @Override @@ -114,6 +117,16 @@ void testCanExecuteNoTarget() { // Show help } + /** + * Test method for {@link AdminTeamKickCommand#canExecute(User, String, List)}. + */ + @Test + void testCanExecuteTooManyArgs() { + AdminTeamKickCommand itl = new AdminTeamKickCommand(ac); + assertFalse(itl.canExecute(user, itl.getLabel(), List.of("a", "b", "c"))); + // Show help + } + /** * Test method for {@link AdminTeamKickCommand#canExecute(User, String, List)}. */ @@ -132,28 +145,118 @@ void testCanExecuteUnknownPlayer() { void testCanExecutePlayerNotInTeam() { AdminTeamKickCommand itl = new AdminTeamKickCommand(ac); when(pm.getUUID(any())).thenReturn(notUUID); - assertFalse(itl.canExecute(user, itl.getLabel(), Collections.singletonList("tastybento"))); + when(im.inTeam(any(), eq(notUUID))).thenReturn(false); + assertFalse(itl.canExecute(user, itl.getLabel(), Collections.singletonList("target"))); verify(user).sendMessage("commands.admin.team.kick.not-in-team"); } /** - * Test method for {@link world.bentobox.bentobox.api.commands.admin.team.AdminTeamKickCommand#execute(User, String, List)}. + * Test that a player in a team on a single island can be kicked with 1 arg. + */ + @Test + void testCanExecuteSuccess() { + AdminTeamKickCommand itl = new AdminTeamKickCommand(ac); + assertTrue(itl.canExecute(user, itl.getLabel(), Collections.singletonList("target"))); + } + + /** + * Test that a player on multiple islands requires an xyz arg. + */ + @Test + void testCanExecuteMultipleIslandsRequiresXyz() { + // Set up island (admin-owned) with a different center location so it gets a unique xyz key + when(island.getOwner()).thenReturn(uuid); + when(island.hasTeam()).thenReturn(true); + org.bukkit.Location loc2 = mock(org.bukkit.Location.class); + when(loc2.toVector()).thenReturn(new Vector(100, 64, 100)); + when(island.getCenter()).thenReturn(loc2); + when(im.getIslands(world, notUUID)).thenReturn(List.of(island, island2)); + AdminTeamKickCommand itl = new AdminTeamKickCommand(ac); + assertFalse(itl.canExecute(user, itl.getLabel(), Collections.singletonList("target"))); + verify(user).sendMessage("commands.admin.unregister.errors.player-has-more-than-one-island"); + } + + /** + * Test that an unknown xyz arg gives an error. + */ + @Test + void testCanExecuteUnknownIslandLocation() { + AdminTeamKickCommand itl = new AdminTeamKickCommand(ac); + assertFalse(itl.canExecute(user, itl.getLabel(), List.of("target", "9,9,9"))); + verify(user).sendMessage("commands.admin.unregister.errors.unknown-island-location"); + } + + /** + * Test that a valid xyz arg selects the correct island. */ @Test - void testExecute() { - when(im.inTeam(any(), any())).thenReturn(true); - String name = "tastybento"; - when(pm.getUUID(any())).thenReturn(uuid); - when(pm.getName(any())).thenReturn(name); + void testCanExecuteWithValidXyz() { + AdminTeamKickCommand itl = new AdminTeamKickCommand(ac); + assertTrue(itl.canExecute(user, itl.getLabel(), List.of("target", XYZ))); + } + /** + * Test method for {@link AdminTeamKickCommand#execute(User, String, List)}. + * Target on one island; kicked with 1 arg. + */ + @Test + void testExecuteSingleIsland() { + String name = "target"; AdminTeamKickCommand itl = new AdminTeamKickCommand(ac); assertTrue(itl.canExecute(user, itl.getLabel(), Collections.singletonList(name))); assertTrue(itl.execute(user, itl.getLabel(), Collections.singletonList(name))); - verify(im, never()).removePlayer(island, uuid); - verify(im).removePlayer(island2, uuid); - verify(user).sendMessage(eq("commands.admin.team.kick.success"), eq(TextVariables.NAME), any(), eq("[owner]"), eq(name)); - // Offline so event will be called 3 times + verify(im).removePlayer(island2, notUUID); + verify(user).sendMessage(eq("commands.admin.team.kick.success"), eq(TextVariables.NAME), any(), eq("[owner]"), + any()); + // 3 events: TeamEvent + IslandEvent (IslandEvent.build fires 2 callEvent calls) + verify(pim, times(3)).callEvent(any()); + } + + /** + * Test method for {@link AdminTeamKickCommand#execute(User, String, List)}. + * Target on multiple islands; kicked with explicit xyz. + */ + @Test + void testExecuteWithXyz() { + AdminTeamKickCommand itl = new AdminTeamKickCommand(ac); + assertTrue(itl.canExecute(user, itl.getLabel(), List.of("target", XYZ))); + assertTrue(itl.execute(user, itl.getLabel(), List.of("target", XYZ))); + verify(im).removePlayer(island2, notUUID); + verify(user).sendMessage(eq("commands.admin.team.kick.success"), eq(TextVariables.NAME), any(), eq("[owner]"), + any()); verify(pim, times(3)).callEvent(any()); } + /** + * Test tab complete with no args returns empty. + */ + @Test + void testTabCompleteNoArgs() { + AdminTeamKickCommand itl = new AdminTeamKickCommand(ac); + Optional> result = itl.tabComplete(user, "", List.of("")); + assertTrue(result.isEmpty()); + } + + /** + * Test tab complete for second arg returns xyz of the target's team islands. + */ + @Test + void testTabCompleteSecondArg() { + when(pm.getUUID("target")).thenReturn(notUUID); + AdminTeamKickCommand itl = new AdminTeamKickCommand(ac); + Optional> result = itl.tabComplete(user, "", List.of("target", "")); + assertTrue(result.isPresent()); + assertTrue(result.get().contains(XYZ)); + } + + /** + * Test tab complete for second arg returns empty when player is unknown. + */ + @Test + void testTabCompleteSecondArgUnknownPlayer() { + when(pm.getUUID("unknown")).thenReturn(null); + AdminTeamKickCommand itl = new AdminTeamKickCommand(ac); + Optional> result = itl.tabComplete(user, "", List.of("unknown", "")); + assertTrue(result.isEmpty()); + } }