diff --git a/src/main/java/world/bentobox/bentobox/api/commands/admin/team/AdminTeamSetownerCommand.java b/src/main/java/world/bentobox/bentobox/api/commands/admin/team/AdminTeamSetownerCommand.java index c812cb789..71f9a69d0 100644 --- a/src/main/java/world/bentobox/bentobox/api/commands/admin/team/AdminTeamSetownerCommand.java +++ b/src/main/java/world/bentobox/bentobox/api/commands/admin/team/AdminTeamSetownerCommand.java @@ -1,9 +1,17 @@ package world.bentobox.bentobox.api.commands.admin.team; import java.util.List; +import java.util.Objects; +import java.util.Optional; import java.util.UUID; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.eclipse.jdt.annotation.Nullable; + +import world.bentobox.bentobox.api.addons.GameModeAddon; import world.bentobox.bentobox.api.commands.CompositeCommand; +import world.bentobox.bentobox.api.commands.ConfirmableCommand; import world.bentobox.bentobox.api.events.island.IslandEvent; import world.bentobox.bentobox.api.events.team.TeamEvent; import world.bentobox.bentobox.api.localization.TextVariables; @@ -17,7 +25,11 @@ import world.bentobox.bentobox.util.Util; * * @author tastybento */ -public class AdminTeamSetownerCommand extends CompositeCommand { +public class AdminTeamSetownerCommand extends ConfirmableCommand { + + private @Nullable UUID targetUUID; + private Island island; + private @Nullable UUID previousOwnerUUID; public AdminTeamSetownerCommand(CompositeCommand parent) { super(parent, "setowner"); @@ -28,35 +40,50 @@ public class AdminTeamSetownerCommand extends CompositeCommand { setPermission("mod.team.setowner"); setParametersHelp("commands.admin.team.setowner.parameters"); setDescription("commands.admin.team.setowner.description"); + this.setOnlyPlayer(true); } @Override - public boolean execute(User user, String label, List args) { + public boolean canExecute(User user, String label, List args) { // If args are not right, show help if (args.size() != 1) { showHelp(this, user); return false; } + // Get target - UUID targetUUID = Util.getUUID(args.get(0)); + targetUUID = Util.getUUID(args.get(0)); if (targetUUID == null) { user.sendMessage("general.errors.unknown-player", TextVariables.NAME, args.get(0)); return false; } - if (!getIslands().inTeam(getWorld(), targetUUID)) { - user.sendMessage("general.errors.not-in-team"); + // Check that user is on an island + Optional opIsland = getIslands().getIslandAt(user.getLocation()); + if (opIsland.isEmpty()) { + user.sendMessage("commands.admin.team.setowner.must-be-on-island"); return false; } - Island island = getIslands().getPrimaryIsland(getWorld(), targetUUID); - UUID previousOwnerUUID = island.getOwner(); + island = opIsland.get(); + previousOwnerUUID = island.getOwner(); if (targetUUID.equals(previousOwnerUUID)) { user.sendMessage("commands.admin.team.setowner.already-owner", TextVariables.NAME, args.get(0)); return false; } + return true; + } - // Get the User corresponding to the current owner + public boolean execute(User user, String label, List args) { + Objects.requireNonNull(island); + Objects.requireNonNull(targetUUID); + + this.askConfirmation(user, user.getTranslation("commands.admin.team.setowner.confirmation", TextVariables.NAME, + args.get(0), TextVariables.XYZ, Util.xyz(island.getCenter().toVector())), () -> changeOwner(user)); + return true; + + } + + protected void changeOwner(User user) { User target = User.getInstance(targetUUID); - // Fire event so add-ons know // Call the setowner event TeamEvent.builder().island(island).reason(TeamEvent.Reason.SETOWNER).involvedPlayer(targetUUID).admin(true) @@ -70,8 +97,20 @@ public class AdminTeamSetownerCommand extends CompositeCommand { .build(); // Make new owner - getIslands().setOwner(getWorld(), user, targetUUID); - user.sendMessage("commands.admin.team.setowner.success", TextVariables.NAME, args.get(0)); + getIslands().setOwner(user, targetUUID, island, RanksManager.MEMBER_RANK); + user.sendMessage("commands.admin.team.setowner.success", TextVariables.NAME, target.getName()); + + // Report if this made player have more islands than expected + // Get how many islands this player has + int num = this.getIslands().getNumberOfConcurrentIslands(targetUUID, getWorld()); + int max = target.getPermissionValue( + this.getIWM().getAddon(getWorld()).map(GameModeAddon::getPermissionPrefix).orElse("") + "island.number", + this.getIWM().getWorldSettings(getWorld()).getConcurrentIslands()); + if (num > max) { + // You cannot make an island + user.sendMessage("commands.admin.team.setowner.extra-islands", TextVariables.NUMBER, String.valueOf(num), + "[max]", String.valueOf(max)); + } // Call the rank change event for the old island owner if (previousOwnerUUID != null) { @@ -80,6 +119,13 @@ public class AdminTeamSetownerCommand extends CompositeCommand { .reason(IslandEvent.Reason.RANK_CHANGE) .rankChange(RanksManager.OWNER_RANK, island.getRank(previousOwnerUUID)).build(); } - return true; + + } + + @Override + public Optional> tabComplete(User user, String alias, List args) { + String lastArg = !args.isEmpty() ? args.get(args.size() - 1) : ""; + List options = Bukkit.getOnlinePlayers().stream().map(Player::getName).toList(); + return Optional.of(Util.tabLimit(options, lastArg)); } } diff --git a/src/main/java/world/bentobox/bentobox/managers/IslandsManager.java b/src/main/java/world/bentobox/bentobox/managers/IslandsManager.java index 08a257c15..fff6cd23a 100644 --- a/src/main/java/world/bentobox/bentobox/managers/IslandsManager.java +++ b/src/main/java/world/bentobox/bentobox/managers/IslandsManager.java @@ -1520,17 +1520,22 @@ public class IslandsManager { /** * Sets this target as the owner for this island * - * @param user previous owner + * @param user user making the change * @param targetUUID new owner * @param island island to register * @param rank rank to which to set old owner. */ public void setOwner(User user, UUID targetUUID, Island island, int rank) { - islandCache.setOwner(island, targetUUID); - // Set old owner as sub-owner on island. - if (rank > RanksManager.VISITOR_RANK) { - island.setRank(user, rank); + // Demote the old owner + if (rank >= RanksManager.OWNER_RANK) { + plugin.logWarning("Setowner: previous owner's rank cannot be higher than SubOwner"); + rank = RanksManager.SUB_OWNER_RANK; } + if (rank > RanksManager.VISITOR_RANK && island.getOwner() != null) { + island.setRank(island.getOwner(), rank); + } + // Make the new owner + islandCache.setOwner(island, targetUUID); user.sendMessage("commands.island.team.setowner.name-is-the-owner", "[name]", plugin.getPlayers().getName(targetUUID)); diff --git a/src/main/java/world/bentobox/bentobox/managers/island/IslandCache.java b/src/main/java/world/bentobox/bentobox/managers/island/IslandCache.java index 5e6896348..360e3b023 100644 --- a/src/main/java/world/bentobox/bentobox/managers/island/IslandCache.java +++ b/src/main/java/world/bentobox/bentobox/managers/island/IslandCache.java @@ -18,6 +18,7 @@ import org.eclipse.jdt.annotation.Nullable; import world.bentobox.bentobox.BentoBox; import world.bentobox.bentobox.api.flags.Flag; import world.bentobox.bentobox.database.objects.Island; +import world.bentobox.bentobox.managers.RanksManager; import world.bentobox.bentobox.util.Util; /** @@ -353,6 +354,7 @@ public class IslandCache { if (newOwnerUUID != null) { islandsByUUID.computeIfAbsent(newOwnerUUID, k -> new HashSet<>()).add(island); } + island.setRank(newOwnerUUID, RanksManager.OWNER_RANK); islandsByLocation.put(island.getCenter(), island); islandsById.put(island.getUniqueId(), island); } diff --git a/src/main/resources/locales/en-US.yml b/src/main/resources/locales/en-US.yml index ac0a963ab..2bbbd78bb 100644 --- a/src/main/resources/locales/en-US.yml +++ b/src/main/resources/locales/en-US.yml @@ -144,7 +144,10 @@ commands: parameters: description: transfers island ownership to the player already-owner: '&c [name] is already the owner of this island!' + must-be-on-island: '&c You must be on the island to set the owner' + confirmation: '&a Are you sure you want to set [name] to be the owner of the island at [xyz]?' success: '&b [name]&a is now the owner of this island.' + extra-islands: '&c Warning: this player now owns [number] islands. This is more than allowed by settings or perms: [max].' range: description: admin island range command invalid-value: diff --git a/src/test/java/world/bentobox/bentobox/api/commands/admin/team/AdminTeamSetownerCommandTest.java b/src/test/java/world/bentobox/bentobox/api/commands/admin/team/AdminTeamSetownerCommandTest.java index ade500c35..86ae24bac 100644 --- a/src/test/java/world/bentobox/bentobox/api/commands/admin/team/AdminTeamSetownerCommandTest.java +++ b/src/test/java/world/bentobox/bentobox/api/commands/admin/team/AdminTeamSetownerCommandTest.java @@ -1,38 +1,45 @@ package world.bentobox.bentobox.api.commands.admin.team; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.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.Arrays; import java.util.HashMap; -import java.util.HashSet; import java.util.List; -import java.util.Set; +import java.util.Optional; import java.util.UUID; import org.bukkit.Bukkit; +import org.bukkit.Location; import org.bukkit.entity.Player; import org.bukkit.plugin.PluginManager; import org.bukkit.scheduler.BukkitScheduler; +import org.bukkit.util.Vector; +import org.eclipse.jdt.annotation.NonNull; 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; import org.powermock.reflect.Whitebox; import world.bentobox.bentobox.BentoBox; +import world.bentobox.bentobox.Settings; +import world.bentobox.bentobox.TestWorldSettings; import world.bentobox.bentobox.api.commands.CompositeCommand; +import world.bentobox.bentobox.api.configuration.WorldSettings; import world.bentobox.bentobox.api.localization.TextVariables; import world.bentobox.bentobox.api.user.User; import world.bentobox.bentobox.database.objects.Island; @@ -40,6 +47,7 @@ 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; @@ -53,16 +61,19 @@ public class AdminTeamSetownerCommandTest { @Mock private CompositeCommand ac; - private UUID uuid; + private UUID uuid = UUID.randomUUID(); @Mock private User user; @Mock private IslandsManager im; @Mock private PlayersManager pm; - private UUID notUUID; + private UUID notUUID = UUID.randomUUID(); @Mock private Island island; + private AdminTeamSetownerCommand itl; + @Mock + private @NonNull Location location; /** */ @@ -73,35 +84,57 @@ public class AdminTeamSetownerCommandTest { Whitebox.setInternalState(BentoBox.class, "instance", plugin); Util.setPlugin(plugin); + Settings settings = new Settings(); + // Settings + when(plugin.getSettings()).thenReturn(settings); + // Command manager CommandsManager cm = mock(CommandsManager.class); when(plugin.getCommandsManager()).thenReturn(cm); // Player Player p = mock(Player.class); + when(p.getUniqueId()).thenReturn(uuid); + when(p.getName()).thenReturn("tastybento"); + User.getInstance(p); // Sometimes use Mockito.withSettings().verboseLogging() when(user.isOp()).thenReturn(false); - uuid = UUID.randomUUID(); - notUUID = UUID.randomUUID(); - while (notUUID.equals(uuid)) { - notUUID = UUID.randomUUID(); - } when(user.getUniqueId()).thenReturn(uuid); when(user.getPlayer()).thenReturn(p); when(user.getName()).thenReturn("tastybento"); + when(user.getTranslation(anyString())) + .thenAnswer((Answer) invocation -> invocation.getArgument(0, String.class)); + when(user.getTranslation(anyString(), anyString(), anyString(), anyString(), anyString())) + .thenAnswer((Answer) invocation -> invocation.getArgument(0, String.class)); User.setPlugin(plugin); + // Locales & Placeholders + LocalesManager lm = mock(LocalesManager.class); + when(lm.get(any(), any())).thenAnswer((Answer) invocation -> invocation.getArgument(1, String.class)); + PlaceholdersManager phm = mock(PlaceholdersManager.class); + when(plugin.getPlaceholdersManager()).thenReturn(phm); + when(phm.replacePlaceholders(any(), any())) + .thenAnswer((Answer) invocation -> invocation.getArgument(1, String.class)); + + when(plugin.getLocalesManager()).thenReturn(lm); + // Parent command has no aliases when(ac.getSubCommandAliases()).thenReturn(new HashMap<>()); // Island World Manager IslandWorldManager iwm = mock(IslandWorldManager.class); when(plugin.getIWM()).thenReturn(iwm); + @NonNull + WorldSettings worldSettings = new TestWorldSettings(); + when(iwm.getWorldSettings(any())).thenReturn(worldSettings); + // Location + when(location.toVector()).thenReturn(new Vector(1, 2, 3)); // 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(island.getOwner()).thenReturn(uuid); + when(island.getCenter()).thenReturn(location); when(im.getPrimaryIsland(any(), any())).thenReturn(island); when(plugin.getIslands()).thenReturn(im); @@ -115,15 +148,13 @@ public class AdminTeamSetownerCommandTest { PowerMockito.mockStatic(Bukkit.class); when(Bukkit.getScheduler()).thenReturn(sch); - // Locales - LocalesManager lm = mock(LocalesManager.class); - when(lm.get(any(), any())).thenReturn("mock translation"); - when(plugin.getLocalesManager()).thenReturn(lm); - // Plugin Manager PluginManager pim = mock(PluginManager.class); when(Bukkit.getPluginManager()).thenReturn(pim); + // DUT + itl = new AdminTeamSetownerCommand(ac); + } @After @@ -133,56 +164,45 @@ public class AdminTeamSetownerCommandTest { } /** - * Test method for {@link AdminTeamSetownerCommand#execute(User, String, List)}. + * Test method for {@link AdminTeamSetownerCommand#canExecute(User, String, List)}. */ @Test public void testExecuteNoTarget() { - AdminTeamSetownerCommand itl = new AdminTeamSetownerCommand(ac); - assertFalse(itl.execute(user, itl.getLabel(), new ArrayList<>())); + assertFalse(itl.canExecute(user, itl.getLabel(), new ArrayList<>())); // Show help + verify(user).sendMessage("commands.help.header", TextVariables.LABEL, "commands.help.console"); } /** - * Test method for {@link AdminTeamSetownerCommand#execute(User, String, List)}. + * Test method for {@link AdminTeamSetownerCommand#setup()} + */ + @Test + public void testSetup() { + assertEquals("commands.admin.team.setowner.description", itl.getDescription()); + assertEquals("commands.admin.team.setowner.parameters", itl.getParameters()); + assertTrue(itl.isOnlyPlayer()); + assertEquals("mod.team.setowner", itl.getPermission()); + } + + /** + * Test method for {@link AdminTeamSetownerCommand#canExecute(User, String, List)}. */ @Test public void testExecuteUnknownPlayer() { - AdminTeamSetownerCommand itl = new AdminTeamSetownerCommand(ac); - String[] name = { "tastybento" }; - when(pm.getUUID(any())).thenReturn(null); - assertFalse(itl.execute(user, itl.getLabel(), Arrays.asList(name))); - verify(user).sendMessage("general.errors.unknown-player", "[name]", name[0]); + assertFalse(itl.canExecute(user, itl.getLabel(), List.of("tastybento"))); + verify(user).sendMessage("general.errors.unknown-player", "[name]", "tastybento"); } /** - * Test method for {@link AdminTeamSetownerCommand#execute(User, String, List)}. - */ - @Test - public void testExecutePlayerNotInTeam() { - AdminTeamSetownerCommand itl = new AdminTeamSetownerCommand(ac); - String[] name = { "tastybento" }; - when(pm.getUUID(any())).thenReturn(notUUID); - // when(im.getMembers(any(), any())).thenReturn(new HashSet<>()); - assertFalse(itl.execute(user, itl.getLabel(), Arrays.asList(name))); - verify(user).sendMessage(eq("general.errors.not-in-team")); - } - - /** - * Test method for {@link AdminTeamSetownerCommand#execute(User, String, List)}. + * Test method for {@link AdminTeamSetownerCommand#canExecute(User, String, List)}. */ @Test public void testExecuteMakeOwnerAlreadyOwner() { - when(im.inTeam(any(), any())).thenReturn(true); - Island is = mock(Island.class); - when(im.getIsland(any(), any(UUID.class))).thenReturn(is); - String[] name = {"tastybento"}; - when(pm.getUUID(any())).thenReturn(notUUID); - when(pm.getName(any())).thenReturn(name[0]); - when(island.getOwner()).thenReturn(notUUID); - - AdminTeamSetownerCommand itl = new AdminTeamSetownerCommand(ac); - assertFalse(itl.execute(user, itl.getLabel(), Arrays.asList(name))); - verify(user).sendMessage("commands.admin.team.setowner.already-owner", TextVariables.NAME, name[0]); + when(im.getIslandAt(any())).thenReturn(Optional.of(island)); + when(island.getOwner()).thenReturn(uuid); + when(Util.getUUID("tastybento")).thenReturn(uuid); + assertFalse(itl.canExecute(user, itl.getLabel(), List.of("tastybento"))); + verify(user).sendMessage("commands.admin.team.setowner.already-owner", TextVariables.NAME, "tastybento"); } /** @@ -190,28 +210,44 @@ public class AdminTeamSetownerCommandTest { */ @Test public void testExecuteSuccess() { - // Player is a team member, not an owner - when(im.hasIsland(any(), any(UUID.class))).thenReturn(false); - when(im.hasIsland(any(), any(User.class))).thenReturn(false); - when(im.inTeam(any(), any())).thenReturn(true); - Island is = mock(Island.class); - when(im.getIsland(any(), any(UUID.class))).thenReturn(is); - String[] name = {"tastybento"}; - when(pm.getUUID(any())).thenReturn(notUUID); - when(pm.getName(any())).thenReturn(name[0]); - // Owner - //when(im.getOwner(any(), eq(notUUID))).thenReturn(uuid); - when(pm.getName(eq(uuid))).thenReturn("owner"); - // Members - Set members = new HashSet<>(); - members.add(uuid); - members.add(notUUID); - //when(im.getMembers(any(), any())).thenReturn(members); + when(im.getIslandAt(any())).thenReturn(Optional.of(island)); + when(island.getOwner()).thenReturn(notUUID); + when(Util.getUUID("tastybento")).thenReturn(uuid); - AdminTeamSetownerCommand itl = new AdminTeamSetownerCommand(ac); - assertTrue(itl.execute(user, itl.getLabel(), Arrays.asList(name))); + assertTrue(itl.canExecute(user, itl.getLabel(), List.of("tastybento"))); + assertTrue(itl.execute(user, itl.getLabel(), List.of("tastybento"))); // Add other verifications - verify(im).setOwner(any(), eq(user), eq(notUUID)); - verify(user).sendMessage("commands.admin.team.setowner.success", TextVariables.NAME, name[0]); + verify(user).getTranslation("commands.admin.team.setowner.confirmation", TextVariables.NAME, "tastybento", + TextVariables.XYZ, "1,2,3"); + } + + /** + * Test method for {@link AdminTeamSetownerCommand#changeOwner(User)} + */ + @Test + public void testChangeOwner() { + when(im.getIslandAt(any())).thenReturn(Optional.of(island)); + when(island.getOwner()).thenReturn(notUUID); + when(Util.getUUID("tastybento")).thenReturn(uuid); + + assertTrue(itl.canExecute(user, itl.getLabel(), List.of("tastybento"))); + itl.changeOwner(user); + // Add other verifications + verify(user).sendMessage("commands.admin.team.setowner.success", TextVariables.NAME, "tastybento"); + } + + /** + * Test method for {@link AdminTeamSetownerCommand#changeOwner(User)} + */ + @Test + public void testChangeOwnerNoOwner() { + when(im.getIslandAt(any())).thenReturn(Optional.of(island)); + when(island.getOwner()).thenReturn(null); + when(Util.getUUID("tastybento")).thenReturn(uuid); + + assertTrue(itl.canExecute(user, itl.getLabel(), List.of("tastybento"))); + itl.changeOwner(user); + // Add other verifications + verify(user).sendMessage("commands.admin.team.setowner.success", TextVariables.NAME, "tastybento"); } }