From da46d3e4b958b7ff0720b27295b1f7ce69f904af Mon Sep 17 00:00:00 2001 From: Tom Hicks Date: Sun, 27 Sep 2026 04:59:16 -0700 Subject: [PATCH] Adds /metropolis-plot-go command to teleport to a plot. --- Human-Tests.md | 9 +- Modrinth-Description.md | 3 +- Project-Description.md | 2 +- README.md | 3 +- TODO.md | 8 +- .../metropolis/MetropolisPlugin.java | 12 +- .../metropolis/PlotReservationLookup.java | 72 +++++++++++ .../commands/MetropolisPlotGoCommand.java | 96 ++++++++++++--- src/main/resources/plugin.yml | 5 +- .../metropolis/PlotReservationLookupTest.java | 83 +++++++++++++ .../commands/MetropolisPlotGoCommandTest.java | 115 ++++++++++++++++++ 11 files changed, 379 insertions(+), 29 deletions(-) create mode 100644 src/main/java/com/majinnaibu/bukkitplugins/metropolis/PlotReservationLookup.java create mode 100644 src/test/java/com/majinnaibu/bukkitplugins/metropolis/PlotReservationLookupTest.java create mode 100644 src/test/java/com/majinnaibu/bukkitplugins/metropolis/commands/MetropolisPlotGoCommandTest.java diff --git a/Human-Tests.md b/Human-Tests.md index 9c63509..afc0be3 100644 --- a/Human-Tests.md +++ b/Human-Tests.md @@ -41,6 +41,11 @@ Use this checklist for behavior that depends on a real Paper world, player inter - As the home owner, build and interact inside the home. As another non-op player, try the same actions. Verify ownership and protection behave as intended in homes, reservations, City, and Spawn. - Verify representative protection flags in-game, including PvP, mob damage/spawning, explosions, and fire/lava behavior where applicable. Run `/metropolis-flag-reset` and confirm the expected flags are restored without changing region bounds or ownership. - Restart the server and confirm reservations and their protection are still present in WorldGuard and remain excluded from home allocation. +- Add a non-op test player as a WorldGuard member of a Metropolis reservation. Run `/metropolis-plot-go` and confirm they arrive inside that reservation at a safe location; repeat with an owner. +- Confirm a player who is neither owner nor member cannot use a reservation name to teleport there. Check that a region they own/member outside homes, City, and Spawn is treated as expected by the Metropolis reservation lookup. +- Add a player to two reservations. Confirm the no-argument form reports both names without teleporting, then specify one reservation and verify the chosen destination. +- As an authorized admin, target another online member with `/metropolis-plot-go `. Confirm the target receives a notification. Repeat without `metropolis.plot.go` and verify targeting another player is denied. +- Test a reservation with no safe destination. Confirm the command reports failure and does not claim teleportation succeeded. Also test console use with an online player name and an unknown/offline name. ### Commands, permissions, and player state @@ -65,7 +70,7 @@ Use this checklist for behavior that depends on a real Paper world, player inter ## Existing Automated Coverage -Run `./gradlew test` for the server-free JUnit suite. It has 20 tests covering: +Run `./gradlew test` for the server-free JUnit suite. It has 26 tests covering: - `CuboidTest`: WorldEdit bound conversion, vector ordering/null handling, touching intersections, and point containment. - `CurrentHomesStoreTest`: UUID-based persistence and resolving a legacy player-name key. @@ -75,6 +80,8 @@ Run `./gradlew test` for the server-free JUnit suite. It has 20 tests covering: - `HomePermissionOverrideTest`: dotted permission-node parsing, priority and tie resolution, invalid entries, and username/permission/default precedence. - `HomeNumberAllocatorTest`: choosing the first unused positive home number. - `MetropolisHomeMoveCommandTest`: selecting a sender's home, selecting an offline target's home, and rejecting invalid requests. +- `PlotReservationLookupTest`: owner/member matching, exclusion of homes/City/Spawn, legacy reservation markers, and region-name aliases. +- `MetropolisPlotGoCommandTest`: self and target teleportation, ambiguous memberships, absent membership, and unsafe-destination feedback. - `MetropolisPlotReserveCommandTest`: parsing six coordinates and rejecting malformed coordinates. - `MetropolisHomeAcquireTest`: acquisition below the limit and refusal at the limit. diff --git a/Modrinth-Description.md b/Modrinth-Description.md index f6f2ff7..b65d0e7 100644 --- a/Modrinth-Description.md +++ b/Modrinth-Description.md @@ -21,6 +21,7 @@ Metropolis targets Paper only; Bukkit and Spigot servers are not supported. - Expands the protected city region as plots are occupied. - Configures plot dimensions, roads, floors, supports, signs, spawn behavior, and perimeter walls. - Reserves named plots by coordinates or a WorldEdit selection. +- Lets WorldGuard owners and members teleport to reservations they belong to, with a named choice when they have multiple. - Tracks home ownership and selected homes by player UUID. - Supports home and plot teleportation, administration, and WorldGuard protection flags. @@ -38,7 +39,7 @@ Metropolis creates `plugins/Metropolis/config.yml` on first startup. Configure p - `/metropolis-home-move [playerName]` - Selects an existing home as active. The player name defaults to the sender; authorized administrators and console can target online or cached offline players. - `/metropolis-home-evict ` - Removes a player's ownership from their selected home region. - `/metropolis-plot-reserve ` - Reserves a region by coordinates; players can use `/metropolis-plot-reserve ` with a WorldEdit selection. -- `/metropolis-plot-go [playerName]` - Teleports to a named plot. +- `/metropolis-plot-go [playerName]` - Teleports to the sender's or an online player's reservation. Specify a reservation name to choose among multiple memberships; `metropolis.plot.go` allows targeting others or bypassing membership. - `/metropolis-flag-reset` - Reapplies Metropolis protection flags. - `/metropolis-debug-generatetesthomes ` - Generates test homes; intended for development servers. diff --git a/Project-Description.md b/Project-Description.md index 3b49e22..7421592 100644 --- a/Project-Description.md +++ b/Project-Description.md @@ -2,4 +2,4 @@ Metropolis turns player onboarding into city planning. As players arrive, the Paper plugin finds the next available plot, builds the surrounding roads and infrastructure, and extends a WorldGuard-protected city that feels designed rather than randomly generated. -The project combines procedural spatial logic, persistent UUID-based ownership, configurable construction rules, WorldEdit-assisted reservations, and a command suite for acquiring and selecting multiple homes. Home limits and plot sizes can be assigned per user or through permission-manager groups using configurable permission tiers. Active-home selections persist across reconnects and server restarts. It is also a modernization case study: a legacy plugin brought forward to Paper 26.2 and 26.3 with Gradle, focused unit tests, and opt-in live-server integration tests. Bukkit and Spigot servers are outside the support target. +The project combines procedural spatial logic, persistent UUID-based ownership, configurable construction rules, WorldEdit-assisted reservations, and a command suite for acquiring and selecting multiple homes. Home limits and plot sizes can be assigned per user or through permission-manager groups using configurable permission tiers. Reservation owners and members can teleport to their reserved plots, with explicit selection when they belong to multiple reservations. Active-home selections persist across reconnects and server restarts. It is also a modernization case study: a legacy plugin brought forward to Paper 26.2 and 26.3 with Gradle, focused unit tests, and opt-in live-server integration tests. Bukkit and Spigot servers are outside the support target. diff --git a/README.md b/README.md index 4cc27de..c8fab55 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ Metropolis expands a protected Minecraft city as players join. Each player can r - Expands the City region as plots are occupied, with optional spawn generation, world-spawn placement, floors, support blocks, signs, and perimeter walls. - Generates roads around plots with configurable width, level, material, clearance, and supports. - Creates named protected reservations from coordinates or a WorldEdit selection so reserved plots are not assigned as homes. +- Lets WorldGuard reservation owners and members teleport to their reservations, with explicit selection when they belong to several. - Persists selected homes by player UUID and recognizes legacy name-based home data during migration. - Supports online/offline player administration for home generation, movement, and eviction. @@ -86,7 +87,7 @@ Assign these permission nodes to groups or individual users in your permission p | `/metropolis-home-move [playerName]` | `metropolis.home.move` | Selects one of a player's existing homes as active. The player argument defaults to the sender; console and authorized admins can target online or cached offline players. The selection persists across restarts. | | `/metropolis-home-evict ` | `metropolis.home.evict` | Removes the player's ownership from their selected home region. | | `/metropolis-plot-reserve ` | `metropolis.plot.reserve` | Creates a named reservation from explicit bounds. A player can instead run `/metropolis-plot-reserve ` with a WorldEdit selection. | -| `/metropolis-plot-go [playerName]` | `metropolis.plot.go` | Teleports the sender or target player to a named occupied or reserved plot. | +| `/metropolis-plot-go [playerName]` | Reservation owner/member; `metropolis.plot.go` to target others or bypass membership | Teleports the sender or an online target to a reservation they own or belong to. If they have several, specify one with `/metropolis-plot-go [playerName]`. | | `/metropolis-flag-reset` | `metropolis.flag.reset` | Reapplies Metropolis protection flags to the City and home regions. | | `/metropolis-debug-generatetesthomes ` | `metropolis.debug` | Generates test homes; intended for development servers. | diff --git a/TODO.md b/TODO.md index 89f9993..10e0e4f 100644 --- a/TODO.md +++ b/TODO.md @@ -35,7 +35,7 @@ | GIT-005 | Planning | Add the possibility to create multiple plot homes in shapes. | | GIT-004 | Planning | Add a command to request home allocation. | | GIT-003 | Ready | Add support for multiple homes. | -| GIT-002 | Planning | Add a command to teleport to a reservation you are part of. | +| GIT-002 | Done | Add a command to teleport to a reservation you are part of. | | GIT-001 | Done | Add a command to move to a different home. | # GIT-001 - Add a command to move to a different home. @@ -46,13 +46,11 @@ `/metropolis-home-move [playerName]` selects one of a player's existing homes as active. Without a player name, the command acts on the player who ran it; console and authorized administrators can specify an online or cached offline player. Reject non-positive or malformed numbers, unknown players, nonexistent homes, and extra arguments with clear feedback. Persist the selection by player UUID and confirm the change to the sender and, when online, the target player. # GIT-002 - Add a command to teleport to a reservation you are part of. -**Status:** Ready +**Status:** Done **Depends On:** [GIT-003](#git-003---add-support-for-multiple-homes) **Description:** -/metropolis-plot-go [playerName] -The target player should be [playerName] if specified and otherwise should be the callee. -The reservation to teleport to is +`/metropolis-plot-go [playerName]` teleports the sender, or the specified online player, to a Metropolis WorldGuard cuboid reservation where that player is an owner or member. If the player belongs to more than one reservation, report the available region names and require an explicit choice with `/metropolis-plot-go [playerName]`. The explicit-name form only permits destinations where the target is a member/owner, unless the sender has `metropolis.plot.go`. Remove the command-level permission gate so reservation members can use self-service teleportation; enforce admin-only target and membership bypass checks in the command. Report unknown players, missing membership, ambiguous reservations, and reservations without a safe teleport location. Do not report success unless teleportation succeeds. ### Comments diff --git a/src/main/java/com/majinnaibu/bukkitplugins/metropolis/MetropolisPlugin.java b/src/main/java/com/majinnaibu/bukkitplugins/metropolis/MetropolisPlugin.java index 80fc7a6..c0b0b3f 100644 --- a/src/main/java/com/majinnaibu/bukkitplugins/metropolis/MetropolisPlugin.java +++ b/src/main/java/com/majinnaibu/bukkitplugins/metropolis/MetropolisPlugin.java @@ -1237,11 +1237,17 @@ public class MetropolisPlugin extends JavaPlugin { public String teleportPlayerToPlot(Player player, Plot plot) { Location loc = plot.getViableSpawnLocation(world); - if (loc != null) { - player.teleport(loc); + if (loc == null) { + return "No safe teleport location exists in plot " + plot.getRegionName() + "."; } + return player.teleport(loc) ? null : "Unable to teleport to plot " + plot.getRegionName() + "."; + } - return null; + public List getReservationsForPlayer(UUID playerId) { + if (regionManager == null) { + return List.of(); + } + return PlotReservationLookup.forPlayer(regionManager.getRegions().values(), playerId); } public boolean homeExists(UUID playerId, int homeNumber) { diff --git a/src/main/java/com/majinnaibu/bukkitplugins/metropolis/PlotReservationLookup.java b/src/main/java/com/majinnaibu/bukkitplugins/metropolis/PlotReservationLookup.java new file mode 100644 index 0000000..9c108ea --- /dev/null +++ b/src/main/java/com/majinnaibu/bukkitplugins/metropolis/PlotReservationLookup.java @@ -0,0 +1,72 @@ +/* +This file is part of Metropolis. + +Metropolis is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Metropolis is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with Metropolis. If not, see . +*/ + +package com.majinnaibu.bukkitplugins.metropolis; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.UUID; + +import com.sk89q.worldguard.protection.flags.Flags; +import com.sk89q.worldguard.protection.flags.StateFlag; +import com.sk89q.worldguard.protection.regions.ProtectedCuboidRegion; +import com.sk89q.worldguard.protection.regions.ProtectedRegion; + +public final class PlotReservationLookup { + private PlotReservationLookup() {} + + public static List forPlayer(Iterable regions, UUID playerId) { + List reservations = new ArrayList<>(); + for (ProtectedRegion region : regions) { + if (!(region instanceof ProtectedCuboidRegion) || !isReservation(region)) { + continue; + } + if (region.getOwners().getUniqueIds().contains(playerId) + || region.getMembers().getUniqueIds().contains(playerId)) { + reservations.add(Plot.get(region)); + } + } + reservations.sort(Comparator.comparing(Plot::getRegionName)); + return List.copyOf(reservations); + } + + public static Plot findByName(List reservations, String requestedName) { + String prefixedName = requestedName.startsWith("r_") ? requestedName : "r_" + requestedName; + for (Plot reservation : reservations) { + if (reservation.getRegionName().equals(requestedName) + || reservation.getRegionName().equals(prefixedName)) { + return reservation; + } + } + return null; + } + + private static boolean isReservation(ProtectedRegion region) { + String regionId = region.getId(); + if (regionId.startsWith("r_")) { + return true; + } + if (regionId.startsWith("h_") + || regionId.equalsIgnoreCase("City") + || regionId.equalsIgnoreCase("Spawn")) { + return false; + } + return region.getFlag(Flags.LAVA_FLOW) == StateFlag.State.DENY + && region.getFlag(Flags.SNOW_FALL) == StateFlag.State.DENY; + } +} diff --git a/src/main/java/com/majinnaibu/bukkitplugins/metropolis/commands/MetropolisPlotGoCommand.java b/src/main/java/com/majinnaibu/bukkitplugins/metropolis/commands/MetropolisPlotGoCommand.java index dd2d615..291bd0b 100644 --- a/src/main/java/com/majinnaibu/bukkitplugins/metropolis/commands/MetropolisPlotGoCommand.java +++ b/src/main/java/com/majinnaibu/bukkitplugins/metropolis/commands/MetropolisPlotGoCommand.java @@ -19,6 +19,7 @@ package com.majinnaibu.bukkitplugins.metropolis.commands; import com.majinnaibu.bukkitplugins.metropolis.MetropolisPlugin; import com.majinnaibu.bukkitplugins.metropolis.Plot; +import com.majinnaibu.bukkitplugins.metropolis.PlotReservationLookup; import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; @@ -34,23 +35,85 @@ public class MetropolisPlotGoCommand implements CommandExecutor { @Override public boolean onCommand(CommandSender sender, Command cmd, String label, String[] args) { - Player player = null; - Plot plot = null; - - if (sender instanceof Player) { - player = (Player) sender; - } - - if (args.length == 1 && player != null) { - plot = _plugin.getPlot(args[0]); - } else if (args.length >= 2) { - player = _plugin.getPlayer(args[1]); - plot = _plugin.getPlot(args[0]); - } else { + if (args.length > 2) { + sender.sendMessage( + "Usage: /metropolis-plot-go [playerName] or /metropolis-plot-go [playerName]"); return false; } - if (plot == null || player == null) { + Player player; + String requestedReservation = null; + if (args.length == 0) { + if (!(sender instanceof Player senderPlayer)) { + sender.sendMessage("Console must specify an online player name."); + return false; + } + player = senderPlayer; + } else if (args.length == 1) { + Player namedPlayer = _plugin.getPlayer(args[0]); + if (namedPlayer != null) { + player = namedPlayer; + } else if (sender instanceof Player senderPlayer) { + player = senderPlayer; + requestedReservation = args[0]; + } else { + sender.sendMessage("No online player named " + args[0] + "."); + return false; + } + } else { + requestedReservation = args[0]; + player = _plugin.getPlayer(args[1]); + if (player == null) { + sender.sendMessage("No online player named " + args[1] + "."); + return false; + } + } + + boolean canManagePlots = sender.hasPermission("metropolis.plot.go"); + if (player != sender && !canManagePlots) { + sender.sendMessage("You do not have permission to teleport another player."); + return false; + } + + var reservations = _plugin.getReservationsForPlayer(player.getUniqueId()); + Plot plot; + if (requestedReservation == null) { + if (reservations.isEmpty()) { + sender.sendMessage( + player.getName() + " is not a member or owner of a Metropolis reservation."); + return false; + } + if (reservations.size() > 1) { + String names = + reservations.stream() + .map(Plot::getRegionName) + .reduce((first, second) -> first + ", " + second) + .orElse(""); + sender.sendMessage( + player.getName() + + " belongs to multiple reservations: " + + names + + ". Specify one with /metropolis-plot-go [playerName]."); + return false; + } + plot = reservations.get(0); + } else { + plot = PlotReservationLookup.findByName(reservations, requestedReservation); + if (plot == null && canManagePlots) { + plot = _plugin.getPlot(requestedReservation); + } + if (plot == null) { + sender.sendMessage( + player.getName() + + " is not a member or owner of reservation " + + requestedReservation + + "."); + return false; + } + } + + if (player != sender && !canManagePlots) { + sender.sendMessage("You do not have permission to teleport another player."); return false; } @@ -60,6 +123,11 @@ public class MetropolisPlotGoCommand implements CommandExecutor { return false; } + String destination = "reservation " + plot.getRegionName(); + sender.sendMessage("Teleported " + player.getName() + " to " + destination + "."); + if (player != sender) { + player.sendMessage("You were teleported to " + destination + "."); + } return true; } } diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index bbf7893..391ca45 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -40,9 +40,8 @@ commands: permission: metropolis.plot.reserve usage: /metropolis-plot-reserve metropolis-plot-go: - description: This command teleports the user or another player to a plot. - permission: metropolis.plot.go - usage: /metropolis-plot-go [playerName] + description: Teleports the sender or an authorized target to a reservation. + usage: /metropolis-plot-go [playerName] or /metropolis-plot-go [playerName] metropolis-debug-generatetesthomes: description: This command is ignored on non-debug builds. permission: metropolis.debug diff --git a/src/test/java/com/majinnaibu/bukkitplugins/metropolis/PlotReservationLookupTest.java b/src/test/java/com/majinnaibu/bukkitplugins/metropolis/PlotReservationLookupTest.java new file mode 100644 index 0000000..8fb67e8 --- /dev/null +++ b/src/test/java/com/majinnaibu/bukkitplugins/metropolis/PlotReservationLookupTest.java @@ -0,0 +1,83 @@ +/* +This file is part of Metropolis. + +Metropolis is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Metropolis is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with Metropolis. If not, see . +*/ + +package com.majinnaibu.bukkitplugins.metropolis; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.List; +import java.util.UUID; + +import com.sk89q.worldedit.math.BlockVector3; +import com.sk89q.worldguard.protection.flags.Flags; +import com.sk89q.worldguard.protection.flags.StateFlag; +import com.sk89q.worldguard.protection.regions.ProtectedCuboidRegion; + +import org.junit.jupiter.api.Test; + +class PlotReservationLookupTest { + private static final UUID PLAYER_ID = UUID.fromString("123e4567-e89b-12d3-a456-426614174000"); + + @Test + void findsReservationsWherePlayerIsOwnerOrMemberButExcludesHomeCityAndSpawn() { + ProtectedCuboidRegion memberReservation = region("r_member"); + memberReservation.getMembers().addPlayer(PLAYER_ID); + ProtectedCuboidRegion ownerReservation = region("private-yard"); + ownerReservation.getOwners().addPlayer(PLAYER_ID); + ProtectedCuboidRegion home = region("h_1_" + PLAYER_ID); + home.getMembers().addPlayer(PLAYER_ID); + ProtectedCuboidRegion city = region("City"); + city.getMembers().addPlayer(PLAYER_ID); + ProtectedCuboidRegion spawn = region("Spawn"); + spawn.getMembers().addPlayer(PLAYER_ID); + ProtectedCuboidRegion unrelated = region("other-region", false); + unrelated.getMembers().addPlayer(PLAYER_ID); + + List reservations = + PlotReservationLookup.forPlayer( + List.of(memberReservation, ownerReservation, home, city, spawn, unrelated), PLAYER_ID); + + assertEquals( + List.of("private-yard", "r_member"), + reservations.stream().map(Plot::getRegionName).toList()); + } + + @Test + void findsNamedReservationWithOrWithoutPrefix() { + ProtectedCuboidRegion reservation = region("r_camp"); + reservation.getMembers().addPlayer(PLAYER_ID); + List reservations = PlotReservationLookup.forPlayer(List.of(reservation), PLAYER_ID); + + assertEquals("r_camp", PlotReservationLookup.findByName(reservations, "camp").getRegionName()); + assertEquals( + "r_camp", PlotReservationLookup.findByName(reservations, "r_camp").getRegionName()); + } + + private static ProtectedCuboidRegion region(String id) { + return region(id, true); + } + + private static ProtectedCuboidRegion region(String id, boolean reservedFlags) { + ProtectedCuboidRegion region = + new ProtectedCuboidRegion(id, BlockVector3.ZERO, BlockVector3.at(5, 5, 5)); + if (reservedFlags) { + region.setFlag(Flags.LAVA_FLOW, StateFlag.State.DENY); + region.setFlag(Flags.SNOW_FALL, StateFlag.State.DENY); + } + return region; + } +} diff --git a/src/test/java/com/majinnaibu/bukkitplugins/metropolis/commands/MetropolisPlotGoCommandTest.java b/src/test/java/com/majinnaibu/bukkitplugins/metropolis/commands/MetropolisPlotGoCommandTest.java new file mode 100644 index 0000000..800ae28 --- /dev/null +++ b/src/test/java/com/majinnaibu/bukkitplugins/metropolis/commands/MetropolisPlotGoCommandTest.java @@ -0,0 +1,115 @@ +/* +This file is part of Metropolis. + +Metropolis is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Metropolis is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with Metropolis. If not, see . +*/ + +package com.majinnaibu.bukkitplugins.metropolis.commands; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.contains; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.List; +import java.util.UUID; + +import com.majinnaibu.bukkitplugins.metropolis.MetropolisPlugin; +import com.majinnaibu.bukkitplugins.metropolis.Plot; +import com.sk89q.worldedit.math.BlockVector3; + +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.junit.jupiter.api.Test; + +class MetropolisPlotGoCommandTest { + private static final UUID PLAYER_ID = UUID.fromString("123e4567-e89b-12d3-a456-426614174000"); + + @Test + void teleportsPlayerToTheirOnlyReservation() { + MetropolisPlugin plugin = mock(MetropolisPlugin.class); + Player player = player("Member", PLAYER_ID); + Plot reservation = plot("r_camp"); + when(plugin.getReservationsForPlayer(PLAYER_ID)).thenReturn(List.of(reservation)); + MetropolisPlotGoCommand command = new MetropolisPlotGoCommand(plugin); + + assertTrue(command.onCommand(player, mock(Command.class), "plot-go", new String[0])); + + verify(plugin).teleportPlayerToPlot(player, reservation); + verify(player).sendMessage(contains("reservation r_camp")); + } + + @Test + void asksForAReservationNameWhenPlayerBelongsToSeveral() { + MetropolisPlugin plugin = mock(MetropolisPlugin.class); + Player player = player("Member", PLAYER_ID); + when(plugin.getReservationsForPlayer(PLAYER_ID)) + .thenReturn(List.of(plot("r_camp"), plot("r_garden"))); + MetropolisPlotGoCommand command = new MetropolisPlotGoCommand(plugin); + + assertFalse(command.onCommand(player, mock(Command.class), "plot-go", new String[0])); + + verify(player).sendMessage(contains("r_camp, r_garden")); + verify(plugin, never()).teleportPlayerToPlot(player, plot("r_camp")); + } + + @Test + void authorizedSenderCanTargetAnOnlineMember() { + MetropolisPlugin plugin = mock(MetropolisPlugin.class); + CommandSender sender = mock(CommandSender.class); + Player target = player("Member", PLAYER_ID); + Plot reservation = plot("r_camp"); + when(sender.hasPermission("metropolis.plot.go")).thenReturn(true); + when(plugin.getPlayer("Member")).thenReturn(target); + when(plugin.getReservationsForPlayer(PLAYER_ID)).thenReturn(List.of(reservation)); + MetropolisPlotGoCommand command = new MetropolisPlotGoCommand(plugin); + + assertTrue(command.onCommand(sender, mock(Command.class), "plot-go", new String[] {"Member"})); + + verify(plugin).teleportPlayerToPlot(target, reservation); + verify(target).sendMessage(contains("You were teleported")); + } + + @Test + void reportsNoMembershipAndUnsafeDestinations() { + MetropolisPlugin plugin = mock(MetropolisPlugin.class); + Player player = player("Member", PLAYER_ID); + when(plugin.getReservationsForPlayer(PLAYER_ID)).thenReturn(List.of()); + MetropolisPlotGoCommand command = new MetropolisPlotGoCommand(plugin); + + assertFalse(command.onCommand(player, mock(Command.class), "plot-go", new String[0])); + verify(player).sendMessage(contains("not a member or owner")); + + Plot reservation = plot("r_camp"); + when(plugin.getReservationsForPlayer(PLAYER_ID)).thenReturn(List.of(reservation)); + when(plugin.teleportPlayerToPlot(player, reservation)).thenReturn("No safe teleport location"); + assertFalse(command.onCommand(player, mock(Command.class), "plot-go", new String[0])); + verify(player).sendMessage(contains("No safe teleport location")); + } + + private static Player player(String name, UUID id) { + Player player = mock(Player.class); + when(player.getName()).thenReturn(name); + when(player.getUniqueId()).thenReturn(id); + return player; + } + + private static Plot plot(String name) { + return new Plot(name, BlockVector3.ZERO, BlockVector3.at(5, 5, 5)); + } +}