From 7794634d37898f0a5775f252e5dc08483e1ee014 Mon Sep 17 00:00:00 2001 From: Josh Roy <10731363+JRoy@users.noreply.github.com> Date: Sat, 4 Dec 2021 09:40:06 -0500 Subject: [PATCH] Add support for multiple queued TPA requests (#3801) This PR adds support for players to receive multiple teleport requests, which are queued and can be managed independently of one another. All commands should retain their current behavior but have some new additions; * `/tpaccept`: now allows you to specify a player or `*` to accept a specific player's or all players' teleport request(s) respectively. - Using a wildcard will only accept all tpahere requests, as players can't teleport to multiple places simultaneously. * `/tpdeny`: now allows you to specify a player or `*` to deny a specific player's or all players' teleport request(s) respectively. This PR also adds a new setting for the maximum amount of pending TPA requests a user can have at once. ```yml # The maximum amount of simultaneous tpa requests that can be pending for any given user. # Once at this threshold, any new tpa requests will bump the oldest tpa requests out of queue. # Defaults to 5. tpa-max-amount: 5 ``` Closes #3769 Closes #1550 Co-authored-by: Mariell Hoversholm Co-authored-by: MD <1917406+mdcfe@users.noreply.github.com> --- .../com/earth2me/essentials/ISettings.java | 2 + .../java/com/earth2me/essentials/IUser.java | 83 ++++++++++ .../com/earth2me/essentials/Settings.java | 5 + .../java/com/earth2me/essentials/User.java | 153 +++++++++++++----- .../essentials/commands/Commandtpa.java | 6 +- .../essentials/commands/Commandtpaall.java | 7 +- .../essentials/commands/Commandtpacancel.java | 17 +- .../essentials/commands/Commandtpaccept.java | 95 ++++++++--- .../essentials/commands/Commandtpahere.java | 5 +- .../essentials/commands/Commandtpdeny.java | 83 +++++++++- .../events/TeleportRequestResponseEvent.java | 90 +++++++++++ Essentials/src/main/resources/config.yml | 5 + .../src/main/resources/messages.properties | 19 ++- Essentials/src/main/resources/plugin.yml | 8 +- 14 files changed, 483 insertions(+), 95 deletions(-) create mode 100644 Essentials/src/main/java/net/essentialsx/api/v2/events/TeleportRequestResponseEvent.java diff --git a/Essentials/src/main/java/com/earth2me/essentials/ISettings.java b/Essentials/src/main/java/com/earth2me/essentials/ISettings.java index c4f409114..405fe5dc0 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/ISettings.java +++ b/Essentials/src/main/java/com/earth2me/essentials/ISettings.java @@ -238,6 +238,8 @@ public interface ISettings extends IConf { long getTpaAcceptCancellation(); + int getTpaMaxRequests(); + long getTeleportInvulnerability(); boolean isTeleportInvulnerability(); diff --git a/Essentials/src/main/java/com/earth2me/essentials/IUser.java b/Essentials/src/main/java/com/earth2me/essentials/IUser.java index c78e18407..797df13e4 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/IUser.java +++ b/Essentials/src/main/java/com/earth2me/essentials/IUser.java @@ -12,13 +12,16 @@ import org.bukkit.Location; import org.bukkit.Material; import org.bukkit.block.Block; import org.bukkit.entity.Player; +import org.checkerframework.checker.nullness.qual.Nullable; import java.math.BigDecimal; + import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.UUID; import java.util.regex.Pattern; /** @@ -61,8 +64,11 @@ public interface IUser { /** * Returns whether this user has an outstanding teleport request to deal with. * + * @deprecated The teleport request system has been moved into a multi-user teleport request queue. + * @see IUser#hasPendingTpaRequests(boolean, boolean) * @return whether there is a teleport request */ + @Deprecated boolean hasOutstandingTeleportRequest(); /** @@ -98,6 +104,11 @@ public interface IUser { boolean canBuild(); + /** + * @deprecated The teleport request system has been moved into a multi-user teleport request queue. + * @see IUser#getNextTpaRequest(boolean, boolean, boolean) + */ + @Deprecated long getTeleportRequestTime(); void enableInvulnerabilityAfterTeleport(); @@ -205,6 +216,8 @@ public interface IUser { String getName(); + UUID getUUID(); + String getDisplayName(); String getFormattedNickname(); @@ -238,4 +251,74 @@ public interface IUser { void setToggleShout(boolean toggleShout); boolean isToggleShout(); + + /** + * Gets information about the most-recently-made, non-expired TPA request in the tpa queue of this {@link IUser}. + *

+ * The TPA Queue is Last-In-First-Out queue which stores all the active pending teleport + * requests of this {@link IUser}. Timeout calculations are also done during the + * iteration process of this method, ensuring that teleport requests made past the timeout + * period are removed from queue and therefore not returned here. The maximum size of this + * queue is determined by {@link ISettings#getTpaMaxRequests()}. + * + * @param inform true if the underlying {@link IUser} should be informed if a request expires during iteration. + * @param performExpirations true if this method should not spend time validating time for all items in the queue and just return the first item in the queue. + * @param excludeHere true if /tphere requests should be ignored in fetching the next tpa request. + * @return A {@link TpaRequest} corresponding to the next available request or null if no valid request is present. + */ + @Nullable TpaRequest getNextTpaRequest(boolean inform, boolean performExpirations, boolean excludeHere); + + /** + * Whether or not this {@link IUser} has any valid TPA requests in queue. + * + * @param inform true if the user should be informed if a request expires during iteration. + * @param excludeHere true if /tpahere requests should be ignored in checking if a tpa request is available. + * @return true if the user has an available pending request in queue. + */ + boolean hasPendingTpaRequests(boolean inform, boolean excludeHere); + + class TpaRequest { + private final String name; + private final UUID requesterUuid; + private boolean here; + private Location location; + private long time; + + public TpaRequest(String name, UUID requesterUuid) { + this.name = name; + this.requesterUuid = requesterUuid; + } + + public String getName() { + return name; + } + + public UUID getRequesterUuid() { + return requesterUuid; + } + + public boolean isHere() { + return here; + } + + public void setHere(boolean here) { + this.here = here; + } + + public Location getLocation() { + return location; + } + + public void setLocation(Location location) { + this.location = location; + } + + public long getTime() { + return time; + } + + public void setTime(long time) { + this.time = time; + } + } } diff --git a/Essentials/src/main/java/com/earth2me/essentials/Settings.java b/Essentials/src/main/java/com/earth2me/essentials/Settings.java index 20ad33b62..47eecc985 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/Settings.java +++ b/Essentials/src/main/java/com/earth2me/essentials/Settings.java @@ -1264,6 +1264,11 @@ public class Settings implements net.ess3.api.ISettings { return config.getLong("tpa-accept-cancellation", 120); } + @Override + public int getTpaMaxRequests() { + return config.getInt("tpa-max-requests", 5); + } + private long _getTeleportInvulnerability() { return config.getLong("teleport-invulnerability", 0) * 1000; } diff --git a/Essentials/src/main/java/com/earth2me/essentials/User.java b/Essentials/src/main/java/com/earth2me/essentials/User.java index 4d161c5c0..039747ad1 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/User.java +++ b/Essentials/src/main/java/com/earth2me/essentials/User.java @@ -31,16 +31,21 @@ import org.bukkit.inventory.PlayerInventory; import org.bukkit.metadata.FixedMetadataValue; import org.bukkit.potion.PotionEffect; import org.bukkit.potion.PotionEffectType; +import org.checkerframework.checker.nullness.qual.Nullable; import java.math.BigDecimal; import java.util.Calendar; +import java.util.Collection; import java.util.GregorianCalendar; +import java.util.Iterator; +import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.UUID; import java.util.WeakHashMap; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; @@ -49,28 +54,37 @@ import static com.earth2me.essentials.I18n.tl; public class User extends UserData implements Comparable, IMessageRecipient, net.ess3.api.IUser { private static final Statistic PLAY_ONE_TICK = EnumUtil.getStatistic("PLAY_ONE_MINUTE", "PLAY_ONE_TICK"); private static final Logger logger = Logger.getLogger("Essentials"); + + // User modules private final IMessageRecipient messageRecipient; private transient final AsyncTeleport teleport; private transient final Teleport legacyTeleport; + + // User command confirmation strings private final Map confirmingPayments = new WeakHashMap<>(); - private transient UUID teleportRequester; - private transient boolean teleportRequestHere; - private transient Location teleportLocation; + + // User teleport variables + private final transient LinkedHashMap teleportRequestQueue = new LinkedHashMap<>(); + + // User properties private transient boolean vanished; - private transient long teleportRequestTime; - private transient long lastOnlineActivity; - private transient long lastThrottledAction; - private transient long lastActivity = System.currentTimeMillis(); private boolean hidden = false; private boolean rightClickJump = false; - private transient Location afkPosition = null; private boolean invSee = false; private boolean recipeSee = false; private boolean enderSee = false; - private transient long teleportInvulnerabilityTimestamp = 0; private boolean ignoreMsg = false; + + // User afk variables private String afkMessage; private long afkSince; + private transient Location afkPosition = null; + + // Misc + private transient long lastOnlineActivity; + private transient long lastThrottledAction; + private transient long lastActivity = System.currentTimeMillis(); + private transient long teleportInvulnerabilityTimestamp = 0; private String confirmingClearCommand; private long lastNotifiedAboutMailsMs; private String lastHomeConfirmation; @@ -91,9 +105,8 @@ public class User extends UserData implements Comparable, IMessageRecipien this.messageRecipient = new SimpleMessageRecipient(ess, this); } - User update(final Player base) { + void update(final Player base) { setBase(base); - return this; } @Override @@ -328,44 +341,95 @@ public class User extends UserData implements Comparable, IMessageRecipien @Override public void requestTeleport(final User player, final boolean here) { - teleportRequestTime = System.currentTimeMillis(); - teleportRequester = player == null ? null : player.getBase().getUniqueId(); - teleportRequestHere = here; - if (player == null) { - teleportLocation = null; - } else { - teleportLocation = here ? player.getLocation() : this.getLocation(); + final TpaRequest request = teleportRequestQueue.getOrDefault(player.getName(), new TpaRequest(player.getName(), player.getUUID())); + request.setTime(System.currentTimeMillis()); + request.setHere(here); + request.setLocation(here ? player.getLocation() : this.getLocation()); + + // Handle max queue size + teleportRequestQueue.remove(request.getName()); + if (teleportRequestQueue.size() >= ess.getSettings().getTpaMaxRequests()) { + String lastKey = null; + for (Map.Entry entry : teleportRequestQueue.entrySet()) { + lastKey = entry.getKey(); + } + teleportRequestQueue.remove(lastKey); } + + // Add request to queue + teleportRequestQueue.put(request.getName(), request); } @Override + @Deprecated public boolean hasOutstandingTeleportRequest() { - if (getTeleportRequest() != null) { // Player has outstanding teleport request. - final long timeout = ess.getSettings().getTpaAcceptCancellation(); - if (timeout != 0) { - if ((System.currentTimeMillis() - getTeleportRequestTime()) / 1000 <= timeout) { // Player has outstanding request - return true; - } else { // outstanding request expired. - requestTeleport(null, false); - return false; + return getNextTpaRequest(false, false, false) != null; + } + + public Collection getPendingTpaKeys() { + return teleportRequestQueue.keySet(); + } + + @Override + public boolean hasPendingTpaRequests(boolean inform, boolean excludeHere) { + return getNextTpaRequest(inform, false, excludeHere) != null; + } + + public boolean hasOutstandingTpaRequest(String playerUsername, boolean here) { + final TpaRequest request = getOutstandingTpaRequest(playerUsername, false); + return request != null && request.isHere() == here; + } + + public @Nullable TpaRequest getOutstandingTpaRequest(String playerUsername, boolean inform) { + if (!teleportRequestQueue.containsKey(playerUsername)) { + return null; + } + + final long timeout = ess.getSettings().getTpaAcceptCancellation(); + final TpaRequest request = teleportRequestQueue.get(playerUsername); + if (timeout < 1 || System.currentTimeMillis() - request.getTime() <= timeout * 1000) { + return request; + } + teleportRequestQueue.remove(playerUsername); + if (inform) { + sendMessage(tl("requestTimedOutFrom", ess.getUser(request.getRequesterUuid()).getDisplayName())); + } + return null; + } + + public TpaRequest removeTpaRequest(String playerUsername) { + return teleportRequestQueue.remove(playerUsername); + } + + @Override + public TpaRequest getNextTpaRequest(boolean inform, boolean performExpirations, boolean excludeHere) { + if (teleportRequestQueue.isEmpty()) { + return null; + } + + final long timeout = ess.getSettings().getTpaAcceptCancellation(); + final Iterator> iterator = teleportRequestQueue.entrySet().iterator(); + TpaRequest nextRequest = null; + while (iterator.hasNext()) { + final TpaRequest request = iterator.next().getValue(); + if (timeout < 1 || (System.currentTimeMillis() - request.getTime()) <= TimeUnit.SECONDS.toMillis(timeout)) { + if (excludeHere && request.isHere()) { + continue; } - } else { // outstanding request does not expire - return true; + + if (performExpirations) { + return request; + } else if (nextRequest == null) { + nextRequest = request; + } + } else { + if (inform) { + sendMessage(tl("requestTimedOutFrom", ess.getUser(request.getRequesterUuid()).getDisplayName())); + } + iterator.remove(); } } - return false; - } - - public UUID getTeleportRequest() { - return teleportRequester; - } - - public boolean isTpRequestHere() { - return teleportRequestHere; - } - - public Location getTpRequestLocation() { - return teleportLocation; + return nextRequest; } public String getNick() { @@ -824,8 +888,11 @@ public class User extends UserData implements Comparable, IMessageRecipien return ess.getPermissionsHandler().canBuild(base, getGroup()); } + @Override + @Deprecated public long getTeleportRequestTime() { - return teleportRequestTime; + final TpaRequest request = getNextTpaRequest(false, false, false); + return request == null ? 0L : request.getTime(); } public boolean isInvSee() { @@ -993,7 +1060,7 @@ public class User extends UserData implements Comparable, IMessageRecipien @Override public UUID getUUID() { - return getBase().getUniqueId(); + return this.getBase().getUniqueId(); } @Override diff --git a/Essentials/src/main/java/com/earth2me/essentials/commands/Commandtpa.java b/Essentials/src/main/java/com/earth2me/essentials/commands/Commandtpa.java index 6233349e6..877e0b1f6 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/commands/Commandtpa.java +++ b/Essentials/src/main/java/com/earth2me/essentials/commands/Commandtpa.java @@ -37,11 +37,12 @@ public class Commandtpa extends EssentialsCommand { if (user.getWorld() != player.getWorld() && ess.getSettings().isWorldTeleportPermissions() && !user.isAuthorized("essentials.worlds." + player.getWorld().getName())) { throw new Exception(tl("noPerm", "essentials.worlds." + player.getWorld().getName())); } + // Don't let sender request teleport twice to the same player. - if (user.getConfigUUID().equals(player.getTeleportRequest()) && player.hasOutstandingTeleportRequest() // Check timeout - && !player.isTpRequestHere()) { // Make sure the last teleport request was actually tpa and not tpahere + if (player.hasOutstandingTpaRequest(user.getName(), false)) { throw new Exception(tl("requestSentAlready", player.getDisplayName())); } + if (player.isAutoTeleportEnabled() && !player.isIgnoredPlayer(user)) { final Trade charge = new Trade(this.getName(), ess); final AsyncTeleport teleport = user.getAsyncTeleport(); @@ -71,6 +72,7 @@ public class Commandtpa extends EssentialsCommand { player.sendMessage(tl("teleportRequestTimeoutInfo", ess.getSettings().getTpaAcceptCancellation())); } } + user.sendMessage(tl("requestSent", player.getDisplayName())); if (user.isAuthorized("essentials.tpacancel")) { user.sendMessage(tl("typeTpacancel")); diff --git a/Essentials/src/main/java/com/earth2me/essentials/commands/Commandtpaall.java b/Essentials/src/main/java/com/earth2me/essentials/commands/Commandtpaall.java index ca3b72896..5b0ec1b8a 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/commands/Commandtpaall.java +++ b/Essentials/src/main/java/com/earth2me/essentials/commands/Commandtpaall.java @@ -19,17 +19,17 @@ public class Commandtpaall extends EssentialsCommand { public void run(final Server server, final CommandSource sender, final String commandLabel, final String[] args) throws Exception { if (args.length < 1) { if (sender.isPlayer()) { - teleportAAllPlayers(server, sender, ess.getUser(sender.getPlayer())); + tpaAll(sender, ess.getUser(sender.getPlayer())); return; } throw new NotEnoughArgumentsException(); } final User target = getPlayer(server, sender, args, 0); - teleportAAllPlayers(server, sender, target); + tpaAll(sender, target); } - private void teleportAAllPlayers(final Server server, final CommandSource sender, final User target) { + private void tpaAll(final CommandSource sender, final User target) { sender.sendMessage(tl("teleportAAll")); for (final User player : ess.getOnlineUsers()) { if (target == player) { @@ -41,6 +41,7 @@ public class Commandtpaall extends EssentialsCommand { if (sender.getSender().equals(target.getBase()) && target.getWorld() != player.getWorld() && ess.getSettings().isWorldTeleportPermissions() && !target.isAuthorized("essentials.worlds." + target.getWorld().getName())) { continue; } + try { final TPARequestEvent tpaEvent = new TPARequestEvent(sender, player, true); ess.getServer().getPluginManager().callEvent(tpaEvent); diff --git a/Essentials/src/main/java/com/earth2me/essentials/commands/Commandtpacancel.java b/Essentials/src/main/java/com/earth2me/essentials/commands/Commandtpacancel.java index 2531b2799..c73e3b7b6 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/commands/Commandtpacancel.java +++ b/Essentials/src/main/java/com/earth2me/essentials/commands/Commandtpacancel.java @@ -1,7 +1,6 @@ package com.earth2me.essentials.commands; import com.earth2me.essentials.User; -import net.ess3.api.IEssentials; import org.bukkit.Server; import static com.earth2me.essentials.I18n.tl; @@ -15,20 +14,12 @@ public class Commandtpacancel extends EssentialsCommand { /** * Cancel {@link User}'s tp request if its {@code requester} is equal to the given {@code requester}. * - * @param ess ess instance * @param user user holding tp request * @param requester tp requester * @return whether tp was cancelled */ - public static boolean cancelTeleportRequest(final IEssentials ess, final User user, final User requester) throws Exception { - if (user.getTeleportRequest() != null) { - final User userRequester = ess.getUser(user.getTeleportRequest()); - if (requester.equals(userRequester)) { - user.requestTeleport(null, false); - return true; - } - } - return false; + public static boolean cancelTeleportRequest(final User user, final User requester) { + return user.removeTpaRequest(requester.getName()) != null; } @Override @@ -37,7 +28,7 @@ public class Commandtpacancel extends EssentialsCommand { int cancellations = 0; for (final User onlineUser : ess.getOnlineUsers()) { if (onlineUser == user) continue; - if (cancelTeleportRequest(ess, onlineUser, user)) { + if (cancelTeleportRequest(onlineUser, user)) { cancellations++; } } @@ -48,7 +39,7 @@ public class Commandtpacancel extends EssentialsCommand { } } else { final User targetPlayer = getPlayer(server, user, args, 0); - if (cancelTeleportRequest(ess, targetPlayer, user)) { + if (cancelTeleportRequest(targetPlayer, user)) { user.sendMessage(tl("teleportRequestSpecificCancelled", targetPlayer.getName())); } } diff --git a/Essentials/src/main/java/com/earth2me/essentials/commands/Commandtpaccept.java b/Essentials/src/main/java/com/earth2me/essentials/commands/Commandtpaccept.java index 77c5ecfc5..cabdeb8ca 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/commands/Commandtpaccept.java +++ b/Essentials/src/main/java/com/earth2me/essentials/commands/Commandtpaccept.java @@ -1,12 +1,18 @@ package com.earth2me.essentials.commands; import com.earth2me.essentials.AsyncTeleport; +import com.earth2me.essentials.IUser; import com.earth2me.essentials.Trade; import com.earth2me.essentials.User; +import net.essentialsx.api.v2.events.TeleportRequestResponseEvent; +import org.bukkit.Bukkit; import org.bukkit.Location; import org.bukkit.Server; import org.bukkit.event.player.PlayerTeleportEvent.TeleportCause; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import java.util.concurrent.CompletableFuture; import static com.earth2me.essentials.I18n.tl; @@ -18,36 +24,87 @@ public class Commandtpaccept extends EssentialsCommand { @Override public void run(final Server server, final User user, final String commandLabel, final String[] args) throws Exception { - final User requester; - try { - requester = ess.getUser(user.getTeleportRequest()); - } catch (final Exception ex) { + final boolean acceptAll; + if (args.length > 0) { + acceptAll = args[0].equals("*") || args[0].equalsIgnoreCase("all"); + } else { + acceptAll = false; + } + + if (!user.hasPendingTpaRequests(true, acceptAll)) { throw new Exception(tl("noPendingRequest")); } + if (args.length > 0) { + if (acceptAll) { + acceptAllRequests(user, commandLabel); + throw new NoChargeException(); + } + user.sendMessage(tl("requestAccepted")); + handleTeleport(user, user.getOutstandingTpaRequest(getPlayer(server, user, args, 0).getName(), true), commandLabel); + } else { + user.sendMessage(tl("requestAccepted")); + handleTeleport(user, user.getNextTpaRequest(true, false, false), commandLabel); + } + throw new NoChargeException(); + } + + private void acceptAllRequests(final User user, final String commandLabel) throws Exception { + IUser.TpaRequest request; + int count = 0; + while ((request = user.getNextTpaRequest(true, true, true)) != null) { + try { + handleTeleport(user, request, commandLabel); + count++; + } catch (Exception e) { + ess.showError(user.getSource(), e, commandLabel); + } finally { + user.removeTpaRequest(request.getName()); + } + } + user.sendMessage(tl("requestAcceptedAll", count)); + } + + @Override + protected List getTabCompleteOptions(Server server, User user, String commandLabel, String[] args) { + if (args.length == 1) { + final List options = new ArrayList<>(user.getPendingTpaKeys()); + options.add("*"); + return options; + } else { + return Collections.emptyList(); + } + } + + private void handleTeleport(final User user, final IUser.TpaRequest request, String commandLabel) throws Exception { + if (request == null) { + throw new Exception(tl("noPendingRequest")); + } + final User requester = ess.getUser(request.getRequesterUuid()); + if (!requester.getBase().isOnline()) { + user.removeTpaRequest(request.getName()); throw new Exception(tl("noPendingRequest")); } - if (user.isTpRequestHere() && ((!requester.isAuthorized("essentials.tpahere") && !requester.isAuthorized("essentials.tpaall")) || (user.getWorld() != requester.getWorld() && ess.getSettings().isWorldTeleportPermissions() && !user.isAuthorized("essentials.worlds." + user.getWorld().getName())))) { + if (request.isHere() && ((!requester.isAuthorized("essentials.tpahere") && !requester.isAuthorized("essentials.tpaall")) || (user.getWorld() != requester.getWorld() && ess.getSettings().isWorldTeleportPermissions() && !user.isAuthorized("essentials.worlds." + user.getWorld().getName())))) { throw new Exception(tl("noPendingRequest")); } - if (!user.isTpRequestHere() && (!requester.isAuthorized("essentials.tpa") || (user.getWorld() != requester.getWorld() && ess.getSettings().isWorldTeleportPermissions() && !user.isAuthorized("essentials.worlds." + requester.getWorld().getName())))) { + if (!request.isHere() && (!requester.isAuthorized("essentials.tpa") || (user.getWorld() != requester.getWorld() && ess.getSettings().isWorldTeleportPermissions() && !user.isAuthorized("essentials.worlds." + requester.getWorld().getName())))) { throw new Exception(tl("noPendingRequest")); } - if (args.length > 0 && !requester.getName().contains(args[0])) { - throw new Exception(tl("noPendingRequest")); - } - - if (!user.hasOutstandingTeleportRequest()) { - user.requestTeleport(null, false); - throw new Exception(tl("requestTimedOut")); + final TeleportRequestResponseEvent event = new TeleportRequestResponseEvent(user, requester, request, true); + Bukkit.getPluginManager().callEvent(event); + if (event.isCancelled()) { + if (ess.getSettings().isDebug()) { + logger.info("TPA accept cancelled by API for " + user.getName() + " (requested by " + requester.getName() + ")"); + } + return; } final Trade charge = new Trade(this.getName(), ess); - user.sendMessage(tl("requestAccepted")); requester.sendMessage(tl("requestAcceptedFrom", user.getDisplayName())); final CompletableFuture future = getNewExceptionFuture(requester.getSource(), commandLabel); @@ -55,8 +112,8 @@ public class Commandtpaccept extends EssentialsCommand { user.sendMessage(tl("pendingTeleportCancelled")); return false; }); - if (user.isTpRequestHere()) { - final Location loc = user.getTpRequestLocation(); + if (request.isHere()) { + final Location loc = request.getLocation(); final AsyncTeleport teleport = requester.getAsyncTeleport(); teleport.setTpType(AsyncTeleport.TeleportType.TPA); future.thenAccept(success -> { @@ -64,14 +121,12 @@ public class Commandtpaccept extends EssentialsCommand { requester.sendMessage(tl("teleporting", loc.getWorld().getName(), loc.getBlockX(), loc.getBlockY(), loc.getBlockZ())); } }); - teleport.teleportPlayer(user, user.getTpRequestLocation(), charge, TeleportCause.COMMAND, future); + teleport.teleportPlayer(user, loc, charge, TeleportCause.COMMAND, future); } else { final AsyncTeleport teleport = requester.getAsyncTeleport(); teleport.setTpType(AsyncTeleport.TeleportType.TPA); teleport.teleport(user.getBase(), charge, TeleportCause.COMMAND, future); } - user.requestTeleport(null, false); - throw new NoChargeException(); + user.removeTpaRequest(request.getName()); } - } diff --git a/Essentials/src/main/java/com/earth2me/essentials/commands/Commandtpahere.java b/Essentials/src/main/java/com/earth2me/essentials/commands/Commandtpahere.java index 5a1a3e474..7d1c95cce 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/commands/Commandtpahere.java +++ b/Essentials/src/main/java/com/earth2me/essentials/commands/Commandtpahere.java @@ -33,11 +33,12 @@ public class Commandtpahere extends EssentialsCommand { if (user.getWorld() != player.getWorld() && ess.getSettings().isWorldTeleportPermissions() && !user.isAuthorized("essentials.worlds." + user.getWorld().getName())) { throw new Exception(tl("noPerm", "essentials.worlds." + user.getWorld().getName())); } + // Don't let sender request teleport twice to the same player. - if (user.getConfigUUID().equals(player.getTeleportRequest()) && player.hasOutstandingTeleportRequest() // Check timeout - && player.isTpRequestHere()) { // Make sure the last teleport request was actually tpahere and not tpa + if (player.hasOutstandingTpaRequest(user.getName(), true)) { throw new Exception(tl("requestSentAlready", player.getDisplayName())); } + if (!player.isIgnoredPlayer(user)) { final TPARequestEvent tpaEvent = new TPARequestEvent(user.getSource(), player, true); ess.getServer().getPluginManager().callEvent(tpaEvent); diff --git a/Essentials/src/main/java/com/earth2me/essentials/commands/Commandtpdeny.java b/Essentials/src/main/java/com/earth2me/essentials/commands/Commandtpdeny.java index d9c23a791..b5c10de51 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/commands/Commandtpdeny.java +++ b/Essentials/src/main/java/com/earth2me/essentials/commands/Commandtpdeny.java @@ -1,8 +1,15 @@ package com.earth2me.essentials.commands; +import com.earth2me.essentials.IUser; import com.earth2me.essentials.User; +import net.essentialsx.api.v2.events.TeleportRequestResponseEvent; +import org.bukkit.Bukkit; import org.bukkit.Server; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + import static com.earth2me.essentials.I18n.tl; public class Commandtpdeny extends EssentialsCommand { @@ -12,16 +19,84 @@ public class Commandtpdeny extends EssentialsCommand { @Override public void run(final Server server, final User user, final String commandLabel, final String[] args) throws Exception { - if (user.getTeleportRequest() == null) { + final boolean denyAll; + if (args.length > 0) { + denyAll = args[0].equals("*") || args[0].equalsIgnoreCase("all"); + } else { + denyAll = false; + } + + if (!user.hasPendingTpaRequests(false, false)) { throw new Exception(tl("noPendingRequest")); } - final User player = ess.getUser(user.getTeleportRequest()); - if (player == null) { + + final IUser.TpaRequest denyRequest; + if (args.length > 0) { + if (denyAll) { + denyAllRequests(user); + return; + } + denyRequest = user.getOutstandingTpaRequest(getPlayer(server, user, args, 0).getName(), false); + } else { + denyRequest = user.getNextTpaRequest(false, true, false); + } + + if (denyRequest == null) { throw new Exception(tl("noPendingRequest")); } + final User player = ess.getUser(denyRequest.getRequesterUuid()); + if (player == null || !player.getBase().isOnline()) { + throw new Exception(tl("noPendingRequest")); + } + + if (sendEvent(user, player, denyRequest)) { + return; + } + user.sendMessage(tl("requestDenied")); player.sendMessage(tl("requestDeniedFrom", user.getDisplayName())); - user.requestTeleport(null, false); + user.removeTpaRequest(denyRequest.getName()); + } + + private void denyAllRequests(User user) { + IUser.TpaRequest request; + int count = 0; + while ((request = user.getNextTpaRequest(false, true, false)) != null) { + final User player = ess.getUser(request.getRequesterUuid()); + + if (sendEvent(user, player, request)) { + continue; + } + + if (player != null && player.getBase().isOnline()) { + player.sendMessage(tl("requestDeniedFrom", user.getDisplayName())); + } + + user.removeTpaRequest(request.getName()); + count++; + } + user.sendMessage(tl("requestDeniedAll", count)); + } + + private boolean sendEvent(User user, User player, IUser.TpaRequest request) { + final TeleportRequestResponseEvent event = new TeleportRequestResponseEvent(user, player, request, false); + Bukkit.getPluginManager().callEvent(event); + final boolean cancelled = event.isCancelled(); + if (cancelled && ess.getSettings().isDebug()) { + logger.info("TPA deny cancelled by API for " + user.getName() + " (requested by " + player.getName() + ")"); + } + return event.isCancelled(); + } + + @Override + protected List getTabCompleteOptions(Server server, User user, String commandLabel, String[] args) { + if (args.length == 1) { + final List options = new ArrayList<>(user.getPendingTpaKeys()); + options.add("*"); + return options; + } else { + return Collections.emptyList(); + } } } diff --git a/Essentials/src/main/java/net/essentialsx/api/v2/events/TeleportRequestResponseEvent.java b/Essentials/src/main/java/net/essentialsx/api/v2/events/TeleportRequestResponseEvent.java new file mode 100644 index 000000000..66bd2cd81 --- /dev/null +++ b/Essentials/src/main/java/net/essentialsx/api/v2/events/TeleportRequestResponseEvent.java @@ -0,0 +1,90 @@ +package net.essentialsx.api.v2.events; + +import com.earth2me.essentials.IUser; +import org.bukkit.event.Cancellable; +import org.bukkit.event.Event; +import org.bukkit.event.HandlerList; + +/** + * Called when a player accepts or denies a teleport. + */ +public class TeleportRequestResponseEvent extends Event implements Cancellable { + private static final HandlerList handlers = new HandlerList(); + + private final net.ess3.api.IUser requestee; + private final net.ess3.api.IUser requester; + private final IUser.TpaRequest tpaRequest; + private final boolean accept; + private boolean canceled = false; + + public TeleportRequestResponseEvent(net.ess3.api.IUser requestee, net.ess3.api.IUser requester, IUser.TpaRequest tpaRequest, boolean accept) { + this.requestee = requestee; + this.requester = requester; + this.tpaRequest = tpaRequest; + this.accept = accept; + } + + /** + * Gets the user who is accepting/denying this teleport request. + * @return the user accepting/denying the request. + */ + public net.ess3.api.IUser getRequestee() { + return requestee; + } + + /** + * Gets the user who submitted this teleport request. + * @return the user who sent the request. + */ + public net.ess3.api.IUser getRequester() { + return requester; + } + + /** + * Gets information about this teleport request. + * @return the {@link com.earth2me.essentials.IUser.TpaRequest} object of this event. + */ + public IUser.TpaRequest getTpaRequest() { + return tpaRequest; + } + + /** + * Whether or not the request has been accepted. + * @return true if accepted, false if denied. + */ + public boolean isAccept() { + return accept; + } + + /** + * Whether or not the request has been denied. + * @return true if denied, false if accepted. + */ + public boolean isDeny() { + return !isAccept(); + } + + @Override + public boolean isCancelled() { + return canceled; + } + + /** + * Sets whether or not to cancel this teleport request. + * Note that cancelling this event will not show a message to users about the cancellation. + * @param cancel whether or not to cancel this teleport request. + */ + @Override + public void setCancelled(boolean cancel) { + this.canceled = cancel; + } + + @Override + public HandlerList getHandlers() { + return handlers; + } + + public static HandlerList getHandlerList() { + return handlers; + } +} diff --git a/Essentials/src/main/resources/config.yml b/Essentials/src/main/resources/config.yml index 58c925e20..bd96fcaf4 100644 --- a/Essentials/src/main/resources/config.yml +++ b/Essentials/src/main/resources/config.yml @@ -698,6 +698,11 @@ jail-online-time: false # Set to 0 for no timeout. tpa-accept-cancellation: 120 +# The maximum number of simultaneous tpa requests that can be pending for any player. +# Once past this threshold, old requests will instantly time out. +# Defaults to 5. +tpa-max-requests: 5 + # Allow players to set hats by clicking on their helmet slot. allow-direct-hat: true diff --git a/Essentials/src/main/resources/messages.properties b/Essentials/src/main/resources/messages.properties index 33275ba59..526dc22db 100644 --- a/Essentials/src/main/resources/messages.properties +++ b/Essentials/src/main/resources/messages.properties @@ -1023,14 +1023,17 @@ replyLastRecipientDisabledFor=\u00a76Replying to last message recipient \u00a7cd replyLastRecipientEnabled=\u00a76Replying to last message recipient \u00a7cenabled\u00a76. replyLastRecipientEnabledFor=\u00a76Replying to last message recipient \u00a7cenabled \u00a76for \u00a7c{0}\u00a76. requestAccepted=\u00a76Teleport request accepted. +requestAcceptedAll=\u00a76Accepted \u00a7c{0} \u00a76pending teleport request(s). requestAcceptedAuto=\u00a76Automatically accepted a teleport request from {0}. requestAcceptedFrom=\u00a7c{0} \u00a76accepted your teleport request. requestAcceptedFromAuto=\u00a7c{0} \u00a76accepted your teleport request automatically. requestDenied=\u00a76Teleport request denied. +requestDeniedAll=\u00a76Denied \u00a7c{0} \u00a76pending teleport request(s). requestDeniedFrom=\u00a7c{0} \u00a76denied your teleport request. requestSent=\u00a76Request sent to\u00a7c {0}\u00a76. requestSentAlready=\u00a74You have already sent {0}\u00a74 a teleport request. requestTimedOut=\u00a74Teleport request has timed out. +requestTimedOutFrom=\u00a74Teleport request from \u00a7c{0} \u00a74has timed out. resetBal=\u00a76Balance has been reset to \u00a7c{0} \u00a76for all online players. resetBalAll=\u00a76Balance has been reset to \u00a7c{0} \u00a76for all players. rest=\u00a76You feel well rested. @@ -1290,10 +1293,14 @@ tpacancelCommandUsage1=/ tpacancelCommandUsage1Description=Cancels all your outstanding teleport requests tpacancelCommandUsage2=/ tpacancelCommandUsage2Description=Cancels all your outstanding teleport request with the specified player -tpacceptCommandDescription=Accepts a teleport request. +tpacceptCommandDescription=Accepts teleport requests. tpacceptCommandUsage=/ [otherplayer] tpacceptCommandUsage1=/ -tpacceptCommandUsage1Description=Accepts an incoming teleport request +tpacceptCommandUsage1Description=Accepts the most recent teleport request +tpacceptCommandUsage2=/ +tpacceptCommandUsage2Description=Accepts a teleport request from the specified player +tpacceptCommandUsage3=/ * +tpacceptCommandUsage3Description=Accepts all teleport requests tpahereCommandDescription=Request that the specified player teleport to you. tpahereCommandUsage=/ tpahereCommandUsage1=/ @@ -1306,10 +1313,14 @@ tpautoCommandDescription=Automatically accept teleportation requests. tpautoCommandUsage=/ [player] tpautoCommandUsage1=/ [player] tpautoCommandUsage1Description=Toggles if tpa requests are auto accepted for yourself or another player if specified -tpdenyCommandDescription=Reject a teleport request. +tpdenyCommandDescription=Rejects teleport requests. tpdenyCommandUsage=/ tpdenyCommandUsage1=/ -tpdenyCommandUsage1Description=Rejects an incoming teleport request +tpdenyCommandUsage1Description=Rejects the most recent teleport request +tpdenyCommandUsage2=/ +tpdenyCommandUsage2Description=Rejects a teleport request from the specified player +tpdenyCommandUsage3=/ * +tpdenyCommandUsage3Description=Rejects all teleport requests tphereCommandDescription=Teleport a player to you. tphereCommandUsage=/ tphereCommandUsage1=/ diff --git a/Essentials/src/main/resources/plugin.yml b/Essentials/src/main/resources/plugin.yml index 45f2607fc..8e60aa7b8 100644 --- a/Essentials/src/main/resources/plugin.yml +++ b/Essentials/src/main/resources/plugin.yml @@ -509,8 +509,8 @@ commands: usage: / aliases: [etpaall] tpaccept: - description: Accepts a teleport request. - usage: / [otherplayer] + description: Accepts teleport requests. + usage: / [player|*] aliases: [etpaccept,tpyes,etpyes] tpahere: description: Request that the specified player teleport to you. @@ -529,8 +529,8 @@ commands: usage: / [player] aliases: [etpacancel] tpdeny: - description: Reject a teleport request. - usage: / + description: Rejects teleport requests. + usage: / [player|*] aliases: [etpdeny,tpno,etpno] tphere: description: Teleport a player to you.